diff --git a/.github/instructions/DnR-protocol.md b/.github/instructions/DnR-protocol.md new file mode 100644 index 0000000..9a9b88f --- /dev/null +++ b/.github/instructions/DnR-protocol.md @@ -0,0 +1,54 @@ +## Deconstruction & Re-architecture Protocol (D&R) + +Mục đích +- D&R là một quy trình 3 giai đoạn để phân tích mọi input (từ một từ tới bản kế hoạch) và chuyển đổi thành các hành động có thể triển khai, an toàn và bền vững. + +Giai đoạn 1 — Phân rã & Hệ thống hóa (Deconstruct) +- Mục tiêu: tách input thành các thành phần cơ bản để loại bỏ nhiễu và từng bước chuẩn hóa dữ liệu. +- Yêu cầu đầu vào tối thiểu (Input Metadata): who, when, context, summary (1 câu), constraints. +- Output: danh sách facts, assumptions, constraints, stakeholders. + +Giai đoạn 2 — Xác định Trọng tâm (Focal) +- Mục tiêu: từ các thành phần đã chuẩn hoá, rút ra tối đa 3 trọng tâm theo tiêu chí impact × confidence. +- Với mỗi trọng tâm cần ghi: lý do ngắn (1 câu), dữ liệu/chứng cứ (nếu có), mức độ ưu tiên (cao/trung/bình). + +Giai đoạn 3 — Tái kiến tạo & Tối ưu (Re-architect) +- Mục tiêu: cho mỗi trọng tâm, đề xuất một Action Card (giải pháp chính + dự phòng) tuân thủ 4 nguyên tắc: Đơn giản, Hiệu quả, Thực dụng, An toàn. +- Action Card (mẫu): + - Tiêu đề: + - Mô tả ngắn (1-2 câu): + - Giải pháp chính: + - Giải pháp dự phòng: + - 3 bước triển khai (từng bước rõ ràng): + - Ước lượng (effort, risk): + - Success metrics (1–2 chỉ số): + +Socratic Gate (Kiểm soát chất lượng trước khi chạy) +- Trước khi triển khai, trả lời 1 câu hỏi Socratic trọng tâm (ví dụ: "Giả định nào nếu sai sẽ khiến giải pháp thất bại?") và hoàn thành checklist an toàn tối thiểu. +- Checklist an toàn (bắt buộc): + 1. Data-safety: dữ liệu nhạy cảm đã được xử lý/ẩn chưa? + 2. Rollback plan: có kế hoạch hoàn tác nếu có lỗi không? + 3. Monitoring: có chỉ số/alert để phát hiện sự cố không? + +Nguyên tắc vận hành +- Ghi rõ mọi giả định; nếu thiếu dữ liệu then chốt, dừng và yêu cầu ít nhất 2 thông tin bổ sung. +- Mỗi đề xuất phải có evidence hoặc một bước kiểm chứng nhanh (smoke test) trước khi triển khai rộng. + +Ví dụ ví dụ ngắn (áp dụng cho "tối ưu build" nhanh) +- Input Metadata: who=devops, when=2025-10-24, summary="Build chậm", constraints="không thay đổi CI cloud". +- Phân rã: tsc full compile, webpack bundle, network download packages. +- Trọng tâm (Top1): Reduce full TypeScript compile time (impact cao × confidence trung). +- Action Card: enable tsc incremental + cache tsc output; Steps: 1) bật --incremental config; 2) add cache dir to CI; 3) monitor compile time. +- Socratic question: Nếu incremental cache bị corrupt, rollback thế nào? Trả lời: revert config và clear cache; CI vẫn chạy full compile. + +How to use +- Khi muốn áp dụng D&R cho một mục tiêu, copy template "Input Metadata" vào comment/issue PR và gọi agent để trả về: (1) Phân rã, (2) Top 3 trọng tâm, (3) Action Cards cho từng trọng tâm, (4) Socratic Gate answers + checklist. + +Vị trí lưu & giao tiếp +- File này là bản canonical; nếu muốn tích hợp template vào scaffolding project, đặt `files/templates/DnR-template.md` hoặc thêm vào generator trong `src/features/creators`. + +Cam kết +- Mọi thay đổi code/commit/push sau khi D&R phải được bạn xác nhận tối thiểu 3 điểm (data-safety, rollback, monitoring). + +--- +Created by AI assistant on branch `feature/add-copilot-instructions` — không push lên remote tự động. diff --git a/.github/instructions/copilot-instructions.md b/.github/instructions/copilot-instructions.md new file mode 100644 index 0000000..519d35a --- /dev/null +++ b/.github/instructions/copilot-instructions.md @@ -0,0 +1,52 @@ +## Copilot / AI contributor instructions — vscode-python-environments + +Quick, actionable notes to get an AI agent productive in this repository. + +- Project type: VS Code extension written in TypeScript. Source under `src/` → built with `webpack` into `./dist/extension.js` (see `package.json` `main`). +- Entry points & API: primary extension code in `src/extension.ts` and public API surface in `src/api.ts` and `src/internal.api.ts`. +- Key directories: `src/` (core), `src/common/` (shared utilities like `localize.ts`, `logging.ts`), `managers/` (env managers), `features/` (functional areas), `examples/` (consumers), `files/templates/` (scaffolding including copilot templates). + +Build / test / run +- Build for development: `npm run compile` (uses `webpack`). +- Watch (dev): `npm run watch` (webpack watch). There is also `npm run watch-tests` (TypeScript watch for tests) and a compound VS Code task `tasks: build` that runs both watchers. +- Compile TypeScript for tests: `npm run compile-tests` (tsc output -> `out`). `npm run pretest` runs `compile-tests` and `compile`. +- Unit tests: `npm run unittest` (Mocha with `build/.mocha.unittests.json`). Run `npm run pretest` first if you need a fresh build. +- Package: `npm run vsce-package` to create a VSIX (requires `vsce`). + +Important repo conventions & patterns +- Localization: All user-facing strings must use the l10n/localize system. See `src/common/localize.ts` and the `l10n` folder. Do not hard-code English strings in UI code. +- Logging: Use the extension logging utilities (`traceLog`, `traceVerbose`) in `src/common/logging.ts`. Avoid `console.log` for internal logs. +- Settings precedence: code assumes VS Code settings precedence (folder → workspace → user). See `.github/instructions/generic.instructions.md` for details. +- Error/UI messages: keep messages actionable and avoid spamming the same message. The repo tracks state to avoid repeated alerts (follow patterns in `src/common/persistentState.ts`). + +Where to change behavior +- Contribution points are defined in `package.json` (`activationEvents`, `contributes.commands`, `configuration`). To add a new command, update `package.json` and implement command registration in `src/extension.ts` or `src/common/commands.ts`. +- Entry build output is `./dist/extension.js`. Small runtime changes should still be made in `src/` and then built (or tested using `tsc`/`webpack` watchers). + +Examples & idioms for code edits +- Add localized strings: put key in `package.nls.json`/`l10n` and use `localize('key', 'Default text')` in code. +- Add telemetry/logging: use helpers in `src/common/telemetry/*` and `traceLog('message', { meta })` from `logging.ts`. +- Tests: unit tests live under `test/`; many tests use mocha + sinon/typemoq/ts-mockito patterns. See `test/unittests.ts` for test harness patterns. + +Integration points & external deps +- Depends on the Python extension (`ms-python.python`) contractually for some features; activation is `onLanguage:python` (see `package.json`). +- Uses `@vscode/test-electron` and `@vscode/test-cli` in dev/test workflows. +- Uses common node deps: `fs-extra`, `dotenv`, `@iarna/toml`, etc. + +When making PRs as an AI +- Keep changes small and focused; follow existing file-level patterns (naming, async/await use, error handling helpers in `src/common/errors`). +- Add docstrings to exported functions (see `.github/instructions/generic.instructions.md`). +- Add localization for any user-visible string. +- Run `npm run lint` and `npm run unittest` (or at minimum `npm run compile-tests` + `npm run compile`) before proposing changes. + +References (where to look) +- `src/` for implementation details and patterns +- `test/` for unit test examples +- `files/templates/copilot-instructions-text/` contains example copilot templates you can reuse +- `package.json` for scripts and contribution points +- `.github/instructions/generic.instructions.md` for project-specific coding rules (localization, logging, docstrings) + +If something is unclear, ask: give the file you intend to edit and a 1–2 line summary of the change. Prefer small incremental PRs. + +--- +Generated/merged automatically by the AI agent. Ask for edits or to expand examples for a specific subfolder. diff --git a/.github/instructions/generic.instructions.md b/.github/instructions/generic.instructions.md new file mode 100644 index 0000000..3f43f20 --- /dev/null +++ b/.github/instructions/generic.instructions.md @@ -0,0 +1,32 @@ +--- +applyTo: '**' +--- + +Provide project context and coding guidelines that AI should follow when generating code, answering questions, or reviewing changes.# Coding Instructions for vscode-python-environments + +## Localization + +- Localize all user-facing messages using VS Code’s `l10n` API. +- Internal log messages do not require localization. + +## Logging + +- Use the extension’s logging utilities (`traceLog`, `traceVerbose`) for internal logs. +- Do not use `console.log` or `console.warn` for logging. + +## Settings Precedence + +- Always consider VS Code settings precedence: + 1. Workspace folder + 2. Workspace + 3. User/global +- Remove or update settings from the highest precedence scope first. + +## Error Handling & User Notifications + +- Avoid showing the same error message multiple times in a session; track state with a module-level variable. +- Use clear, actionable error messages and offer relevant buttons (e.g., "Open settings", "Close"). + +## Documentation + +- Add clear docstrings to public functions, describing their purpose, parameters, and behavior. diff --git a/.github/instructions/issue-format.instructions.md b/.github/instructions/issue-format.instructions.md new file mode 100644 index 0000000..69ea66b --- /dev/null +++ b/.github/instructions/issue-format.instructions.md @@ -0,0 +1,84 @@ +--- +applyTo: '**/github-issues/**' +--- + +# Guidelines for Creating Effective GitHub Issues + +## Issue Format + +When creating GitHub issues, use the following structure to ensure clarity and ease of verification: + +### For Bug Reports + +1. **Title**: Concise description of the issue (5-10 words) + +2. **Problem Statement**: + + - 1-2 sentences describing the issue + - Focus on user impact + - Use clear, non-technical language when possible + +3. **Steps to Verify Fix**: + - Numbered list (5-7 steps maximum) + - Start each step with an action verb + - Include expected observations + - Cover both success paths and cancellation/back button scenarios + +### For Feature Requests + +1. **Title**: Clear description of the requested feature + +2. **Need Statement**: + + - 1-2 sentences describing the user need + - Explain why this feature would be valuable + +3. **Acceptance Criteria**: + - Bulleted list of verifiable behaviors + - Include how a user would confirm the feature works as expected + +## Examples + +### Bug Report Example + +``` +# Terminal opens prematurely with PET Resolve Environment command + +**Problem:** When using "Resolve Environment..." from the Python Environment Tool menu, +the terminal opens before entering a path, creating a confusing workflow. + +**Steps to verify fix:** +1. Run "Python Environments: Run Python Environment Tool in Terminal" from Command Palette +2. Select "Resolve Environment..." +3. Verify no terminal opens yet +4. Enter a Python path +5. Verify terminal only appears after path entry +6. Try canceling at the input step - confirm no terminal appears +``` + +### Feature Request Example + +``` +# Add back button support to multi-step UI flows + +**Problem:** The UI flows for environment creation and Python project setup lack back button +functionality, forcing users to cancel and restart when they need to change a previous selection. + +**Steps to verify implementation:** +1. Test back button in PET workflow: Run "Python Environments: Run Python Environment Tool in Terminal", + select "Resolve Environment...", press back button, confirm it returns to menu +2. Test back button in VENV creation: Run "Create environment", select VENV, press back button at various steps +3. Test back button in CONDA creation: Create CONDA environment, use back buttons to navigate between steps +4. Test back button in Python project flow: Add Python project, verify back functionality in project type selection +``` + +## Best Practices + +1. **Be concise**: Keep descriptions short but informative +2. **Use active voice**: "Terminal opens prematurely" rather than "The terminal is opened prematurely" +3. **Include context**: Mention relevant commands, UI elements, and workflows +4. **Focus on verification**: Make steps actionable and observable +5. **Cover edge cases**: Include cancellation paths and error scenarios +6. **Use formatting**: Bold headings and numbered lists improve readability + +Remember that good issues help both developers fixing problems and testers verifying solutions. diff --git a/.github/instructions/repo-analysis-report.md b/.github/instructions/repo-analysis-report.md new file mode 100644 index 0000000..3dc491a --- /dev/null +++ b/.github/instructions/repo-analysis-report.md @@ -0,0 +1,108 @@ +## Báo cáo phân tích kho mã (D&R) — Phân tích sơ bộ dựa trên dữ liệu local + +Lưu ý ngắn: báo cáo này được tạo bằng giao thức D&R và dựa trên dữ liệu có sẵn cục bộ trong môi trường làm việc (local clones) và metadata của remote được cấu hình trong những repo đó. Tôi chưa truy vấn GitHub API cho toàn bộ tài khoản vì `gh` chưa được xác thực trong môi trường này. Ở cuối báo cáo có hướng dẫn để thu thập phân tích đầy đủ (yêu cầu đăng nhập `gh` hoặc cung cấp token API). + +Input Metadata +- who: owner (sowhat1989) / AI-assistant (người phân tích) +- when: 2025-10-24 +- context: workspace local chứa clone của `vscode-python-environments` (repo fork của microsoft/vscode-python-environments) +- constraints: không có quyền API GitHub trong môi trường hiện tại (gh chưa login) +- summary: phân tích sơ bộ các kho mã trên account dựa trên clone(s) local và metadata remote + +Giai đoạn 1 — Phân rã (Deconstruct) +- Evidence (từ local scan): + - Found local clones: + - `/Users/andy/Documents/GitHub/vscode-python-environments` (has remote `origin` and `upstream`) + - nested repo `/Users/andy/Documents/GitHub/vscode-python-environments/vscode-python-environments` (same origin) + - Remote `origin` => https://github.com/sowhat1989/vscode-python-environments.git + - Remote `upstream` => https://github.com/microsoft/vscode-python-environments.git + - Recent commits show activity and tests in repository; unit tests run locally: 219 passing, 1 pending. +- Assumptions (explicit): + 1. Các kho còn lại (nếu có) không được clone trong thư mục quét nên không thể phân tích ở bước này. + 2. `origin` là fork của `microsoft/vscode-python-environments` (evidence: upstream remote configured). + +Giai đoạn 2 — Xác định Trọng tâm (Focal) +- Top1: Verify & improve contribution readiness for the forked repo + - Lý do: có feature branch `feature/add-copilot-instructions` với docs mới; PR có thể sắp được tạo. + - Evidence: local branch, commits, build + unit tests pass locally. + - Priority: Cao +- Top2: Ensure CI / metadata & privacy hygiene + - Lý do: Push ban đầu bị chặn bởi GH007 (private email) — đã rewrite history to use noreply; cần quy trình để tránh lặp lại. + - Evidence: earlier push rejected by GH007; history rewrite performed. + - Priority: Trung +- Top3: Create standardized contributor docs & generator templates (D&R + Copilot templates) + - Lý do: repo chứa templates under `.github/instructions` and `files/templates/` — standardization will speed up future contributions and agent interactions. + - Evidence: created `.github/instructions/DnR-protocol.md`, copilot template files in repo. + - Priority: Trung + +Giai đoạn 3 — Action Cards (Re-architect) + +- Action Card A: Prepare PR and contribution pipeline + - Tiêu đề: Mở Pull Request cho `feature/add-copilot-instructions` + - Mô tả: Tạo PR có nội dung: D&R protocol doc, copilot instructions template, kèm kết quả build/tests. + - Giải pháp chính: + 1. Tạo PR trên GitHub từ branch `feature/add-copilot-instructions` -> `main` với mô tả chi tiết. + 2. Đính kèm kết quả `npm run pretest && npm run unittest` (219 passing, 1 pending). + 3. Request review từ maintainers (tag `@microsoft` / `@EleanorBoyd` nếu appropriate) hoặc người duy trì repo fork. + - Giải pháp dự phòng: nếu maintainers yêu cầu chỉnh sửa, tạo thêm commit sửa và rebase/squash theo hướng dẫn. + - 3 bước triển khai: + 1. Kiểm tra và chạy lint (npm run lint) — fix nếu có lỗi. + 2. Tạo PR bằng GitHub UI hoặc `gh pr create --fill`. + 3. Theo dõi CI/PR feedback và address comments. + - Ước lượng: effort = low (docs + templates), risk = low + - Success metrics: PR merged; no CI failures; reviewers approve. + +- Action Card B: Add a short CONTRIBUTING.md + privacy checklist + - Tiêu đề: CONTRIBUTING + privacy guidelines + - Mô tả: Thêm file `CONTRIBUTING.md` mô tả steps for contributions, required checks (unit tests, lint), and Git config tips (noreply email). + - Giải pháp chính: + 1. Create `CONTRIBUTING.md` with sections: Setup, Build & Tests, Commit conventions, How to avoid GH007 and how to rewrite safely. + - Giải pháp dự phòng: If contributors still push private emails, add pre-push git hook to check author email (or add CI check that rejects commits with disallowed emails). + - 3 bước triển khai: + 1. Draft CONTRIBUTING.md and commit to feature branch. + 2. Add optional `.githooks/pre-push` (documented) to check git config user.email. + 3. Document steps to fix history and to use `--force-with-lease` safely. + - Ước lượng: effort = low-medium, risk = low + - Success metrics: fewer GH007 incidents; contributors follow conventions. + +- Action Card C: Automate account-level inventory (next-step) + - Tiêu đề: Full account inventory & activity analysis (requires auth) + - Mô tả: Run an authenticated scan of GitHub account to list repos, languages, recent activity, open PRs, archived repos. + - Giải pháp chính: + 1. Use `gh` CLI (recommended) or GitHub API with a personal access token to list repos and fetch metadata. + 2. Aggregate stats (languages, last push date, topics, CI presence). + - Giải pháp dự phòng: If `gh` cannot be used, run this scan manually from your machine and provide JSON output. + - 3 bước triển khai: + 1. Authenticate: `gh auth login` or export `GITHUB_TOKEN` (scopes: repo/read) + 2. Run: `gh repo list sowhat1989 --limit 500 --json name,fullName,description,visibility,pushedAt,primaryLanguage,defaultBranchRef --jq '.' > repos.json` + 3. Run per-repo queries for CI/workflows and recent commits; generate summarized report. + - Ước lượng: effort = medium, risk = minimal (read-only) + - Success metrics: generated JSON inventory + report summarizing activity and suggestions. + +Socratic Gate (pre-deploy checklist) + - Socratic question: Nếu tôi đánh giá sai rằng repo này là fork và maintainer chính sẽ chấp nhận PR, hậu quả là gì? + - Nếu sai: PR có thể bị đóng hoặc không được phản hồi; thời gian bị lãng phí. Mitigation: tag maintainers, chạy tests, giữ PR nhỏ và dễ review. + - Checklist an toàn (bắt buộc): + 1. Data-safety: không public secrets; hiện tại không phát hiện secrets trong các thay đổi docs. + 2. Rollback plan: nếu cần undo, branch còn nguyên và có thể revert commit hoặc reset branch remote. + 3. Monitoring: theo dõi CI/PR checks và notifications. + +Verification steps tôi đã thực hiện (local) + - Scans run: + - `find /Users/andy/Documents/GitHub -maxdepth 3 -type d -name .git` → tìm local clones (chỉ tìm thấy repo hiện tại) + - `git remote -v` và `git log -n 5` tại repo root → xác thực `origin` + `upstream` remotes, recent commits + - Ran `npm run pretest && npm run unittest` locally → 219 passing, 1 pending + - Kết luận: dữ liệu local cho thấy repo là fork của microsoft/vscode-python-environments và có activity + tests passing. Không có dữ liệu khác từ tài khoản GitHub vì `gh` chưa login. + +Next steps (actionable) +1. Nếu bạn muốn phân tích TOÀN BỘ tài khoản GitHub, hãy cho phép tôi truy cập read-only bằng một trong các cách: + - Chạy `gh auth login` trên máy này (tôi sẽ re-run `gh repo list ...`) *hoặc* + - Tạo Personal Access Token và đặt vào `GITHUB_TOKEN` trong environment (tùy bạn), rồi tôi sẽ chạy API queries. +2. Tôi có thể tạo PR tự động cho `feature/add-copilot-instructions` (tôi đã push branch) — muốn tôi tạo PR không? +3. Tôi có thể thêm `CONTRIBUTING.md` và `repo-analysis-report.md` commit lên branch hiện tại. + +Assumptions & Limitations + - Đây là phân tích SƠ BỘ, chỉ dùng dữ liệu local và các remotes cấu hình. Để có báo cáo đầy đủ cho toàn bộ tài khoản, cần đăng nhập GitHub CLI hoặc cung cấp token read-only. + +--- +Generated by assistant using D&R on branch `feature/add-copilot-instructions` on 2025-10-24. diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md new file mode 100644 index 0000000..3ecbdcc --- /dev/null +++ b/.github/instructions/testing-workflow.instructions.md @@ -0,0 +1,553 @@ +--- +applyTo: '**/test/**' +--- + +# AI Testing Workflow Guide: Write, Run, and Fix Tests + +This guide provides comprehensive instructions for AI agents on the complete testing workflow: writing tests, running them, diagnosing failures, and fixing issues. Use this guide whenever working with test files or when users request testing tasks. + +## Complete Testing Workflow + +This guide covers the full testing lifecycle: + +1. **📝 Writing Tests** - Create comprehensive test suites +2. **▶️ Running Tests** - Execute tests using VS Code tools +3. **🔍 Diagnosing Issues** - Analyze failures and errors +4. **🛠️ Fixing Problems** - Resolve compilation and runtime issues +5. **✅ Validation** - Ensure coverage and resilience + +### When to Use This Guide + +**User Requests Testing:** + +- "Write tests for this function" +- "Run the tests" +- "Fix the failing tests" +- "Test this code" +- "Add test coverage" + +**File Context Triggers:** + +- Working in `**/test/**` directories +- Files ending in `.test.ts` or `.unit.test.ts` +- Test failures or compilation errors +- Coverage reports or test output analysis + +## Test Types + +When implementing tests as an AI agent, choose between two main types: + +### Unit Tests (`*.unit.test.ts`) + +- **Fast isolated testing** - Mock all external dependencies +- **Use for**: Pure functions, business logic, data transformations +- **Execute with**: `runTests` tool with specific file patterns +- **Mock everything** - VS Code APIs automatically mocked via `/src/test/unittests.ts` + +### Extension Tests (`*.test.ts`) + +- **Full VS Code integration** - Real environment with actual APIs +- **Use for**: Command registration, UI interactions, extension lifecycle +- **Execute with**: VS Code launch configurations or `runTests` tool +- **Slower but comprehensive** - Tests complete user workflows + +## 🤖 Agent Tool Usage for Test Execution + +### Primary Tool: `runTests` + +Use the `runTests` tool to execute tests programmatically: + +```typescript +// Run specific test files +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'run', +}); + +// Run tests with coverage +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'coverage', + coverageFiles: ['/absolute/path/to/source.ts'], +}); + +// Run specific test names +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + testNames: ['should handle edge case', 'should validate input'], +}); +``` + +### Compilation Requirements + +Before running tests, ensure compilation: + +```typescript +// Start watch mode for auto-compilation +await run_in_terminal({ + command: 'npm run watch-tests', + isBackground: true, + explanation: 'Start test compilation in watch mode', +}); + +// Or compile manually +await run_in_terminal({ + command: 'npm run compile-tests', + isBackground: false, + explanation: 'Compile TypeScript test files', +}); +``` + +### Alternative: Terminal Execution + +For targeted test runs when `runTests` tool is unavailable: + +```typescript +// Run specific test suite +await run_in_terminal({ + command: 'npm run unittest -- --grep "Suite Name"', + isBackground: false, + explanation: 'Run targeted unit tests', +}); +``` + +## 🔍 Diagnosing Test Failures + +### Common Failure Patterns + +**Compilation Errors:** + +```typescript +// Missing imports +if (error.includes('Cannot find module')) { + await addMissingImports(testFile); +} + +// Type mismatches +if (error.includes("Type '" && error.includes("' is not assignable"))) { + await fixTypeIssues(testFile); +} +``` + +**Runtime Errors:** + +```typescript +// Mock setup issues +if (error.includes('stub') || error.includes('mock')) { + await fixMockConfiguration(testFile); +} + +// Assertion failures +if (error.includes('AssertionError')) { + await analyzeAssertionFailure(error); +} +``` + +### Systematic Failure Analysis + +```typescript +interface TestFailureAnalysis { + type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; + message: string; + location: { file: string; line: number; col: number }; + suggestedFix: string; +} + +function analyzeFailure(failure: TestFailure): TestFailureAnalysis { + if (failure.message.includes('Cannot find module')) { + return { + type: 'compilation', + message: failure.message, + location: failure.location, + suggestedFix: 'Add missing import statement', + }; + } + // ... other failure patterns +} +``` + +### Agent Decision Logic for Test Type Selection + +**Choose Unit Tests (`*.unit.test.ts`) when analyzing:** + +- Functions with clear inputs/outputs and no VS Code API dependencies +- Data transformation, parsing, or utility functions +- Business logic that can be isolated with mocks +- Error handling scenarios with predictable inputs + +**Choose Extension Tests (`*.test.ts`) when analyzing:** + +- Functions that register VS Code commands or use `vscode.*` APIs +- UI components, tree views, or command palette interactions +- File system operations requiring workspace context +- Extension lifecycle events (activation, deactivation) + +**Agent Implementation Pattern:** + +```typescript +function determineTestType(functionCode: string): 'unit' | 'extension' { + if ( + functionCode.includes('vscode.') || + functionCode.includes('commands.register') || + functionCode.includes('window.') || + functionCode.includes('workspace.') + ) { + return 'extension'; + } + return 'unit'; +} +``` + +## 🎯 Step 1: Automated Function Analysis + +As an AI agent, analyze the target function systematically: + +### Code Analysis Checklist + +```typescript +interface FunctionAnalysis { + name: string; + inputs: string[]; // Parameter types and names + outputs: string; // Return type + dependencies: string[]; // External modules/APIs used + sideEffects: string[]; // Logging, file system, network calls + errorPaths: string[]; // Exception scenarios + testType: 'unit' | 'extension'; +} +``` + +### Analysis Implementation + +1. **Read function source** using `read_file` tool +2. **Identify imports** - look for `vscode.*`, `child_process`, `fs`, etc. +3. **Map data flow** - trace inputs through transformations to outputs +4. **Catalog dependencies** - external calls that need mocking +5. **Document side effects** - logging, file operations, state changes + +### Test Setup Differences + +#### Unit Test Setup (\*.unit.test.ts) + +```typescript +// Mock VS Code APIs - handled automatically by unittests.ts +import * as sinon from 'sinon'; +import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions + +// Stub wrapper functions, not VS Code APIs directly +const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); +``` + +#### Extension Test Setup (\*.test.ts) + +```typescript +// Use real VS Code APIs +import * as vscode from 'vscode'; + +// Real VS Code APIs available - no mocking needed +const config = vscode.workspace.getConfiguration('python'); +``` + +## 🎯 Step 2: Generate Test Coverage Matrix + +Based on function analysis, automatically generate comprehensive test scenarios: + +### Coverage Matrix Generation + +```typescript +interface TestScenario { + category: 'happy-path' | 'edge-case' | 'error-handling' | 'side-effects'; + description: string; + inputs: Record; + expectedOutput?: any; + expectedSideEffects?: string[]; + shouldThrow?: boolean; +} +``` + +### Automated Scenario Creation + +1. **Happy Path**: Normal execution with typical inputs +2. **Edge Cases**: Boundary conditions, empty/null inputs, unusual but valid data +3. **Error Scenarios**: Invalid inputs, dependency failures, exception paths +4. **Side Effects**: Verify logging calls, file operations, state changes + +### Agent Pattern for Scenario Generation + +```typescript +function generateTestScenarios(analysis: FunctionAnalysis): TestScenario[] { + const scenarios: TestScenario[] = []; + + // Generate happy path for each input combination + scenarios.push(...generateHappyPathScenarios(analysis)); + + // Generate edge cases for boundary conditions + scenarios.push(...generateEdgeCaseScenarios(analysis)); + + // Generate error scenarios for each dependency + scenarios.push(...generateErrorScenarios(analysis)); + + return scenarios; +} +``` + +## 🗺️ Step 3: Plan Your Test Coverage + +### Create a Test Coverage Matrix + +#### Main Flows + +- ✅ **Happy path scenarios** - normal expected usage +- ✅ **Alternative paths** - different configuration combinations +- ✅ **Integration scenarios** - multiple features working together + +#### Edge Cases + +- 🔸 **Boundary conditions** - empty inputs, missing data +- 🔸 **Error scenarios** - network failures, permission errors +- 🔸 **Data validation** - invalid inputs, type mismatches + +#### Real-World Scenarios + +- ✅ **Fresh install** - clean slate +- ✅ **Existing user** - migration scenarios +- ✅ **Power user** - complex configurations +- 🔸 **Error recovery** - graceful degradation + +### Example Test Plan Structure + +```markdown +## Test Categories + +### 1. Configuration Migration Tests + +- No legacy settings exist +- Legacy settings already migrated +- Fresh migration needed +- Partial migration required +- Migration failures + +### 2. Configuration Source Tests + +- Global search paths +- Workspace search paths +- Settings precedence +- Configuration errors + +### 3. Path Resolution Tests + +- Absolute vs relative paths +- Workspace folder resolution +- Path validation and filtering + +### 4. Integration Scenarios + +- Combined configurations +- Deduplication logic +- Error handling flows +``` + +## 🔧 Step 4: Set Up Your Test Infrastructure + +### Test File Structure + +```typescript +// 1. Imports - group logically +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// 2. Function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +// 3. Mock interfaces +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} +``` + +### Mock Setup Strategy + +```typescript +suite('Function Integration Tests', () => { + // 1. Declare all mocks + let mockGetConfiguration: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + let mockTraceLog: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + + // 2. Mock complex objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // 3. Initialize all mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockTraceLog = sinon.stub(logging, 'traceLog'); + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // 4. Set up default behaviors + mockGetWorkspaceFolders.returns(undefined); + + // 5. Create mock configuration objects + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + }); + + teardown(() => { + sinon.restore(); // Always clean up! + }); +}); +``` + +## Step 4: Write Tests Using Mock → Run → Assert Pattern + +### The Three-Phase Pattern + +#### Phase 1: Mock (Set up the scenario) + +```typescript +test('Description of what this tests', async () => { + // Mock → Clear description of the scenario + pythonConfig.inspect.withArgs('venvPath').returns({ globalValue: '/path' }); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + mockGetWorkspaceFolders.returns([{ uri: Uri.file('/workspace') }]); +``` + +#### Phase 2: Run (Execute the function) + +```typescript +// Run +const result = await getAllExtraSearchPaths(); +``` + +#### Phase 3: Assert (Verify the behavior) + +```typescript + // Assert - Use set-based comparison for order-agnostic testing + const expected = new Set(['/expected', '/paths']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + + // Verify side effects + assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); +}); +``` + +## Step 6: Make Tests Resilient + +### Use Order-Agnostic Comparisons + +```typescript +// ❌ Brittle - depends on order +assert.deepStrictEqual(result, ['/path1', '/path2', '/path3']); + +// ✅ Resilient - order doesn't matter +const expected = new Set(['/path1', '/path2', '/path3']); +const actual = new Set(result); +assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); +assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); +``` + +### Use Flexible Error Message Testing + +```typescript +// ❌ Brittle - exact text matching +assert(mockTraceError.calledWith('Error during legacy python settings migration:')); + +// ✅ Resilient - pattern matching +assert(mockTraceError.calledWith(sinon.match.string, sinon.match.instanceOf(Error)), 'Should log migration error'); + +// ✅ Resilient - key terms with regex +assert(mockTraceError.calledWith(sinon.match(/migration.*error/i)), 'Should log migration error'); +``` + +### Handle Complex Mock Scenarios + +```typescript +// For functions that call the same mock multiple times +envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); +envConfig.inspect + .withArgs('globalSearchPaths') + .onSecondCall() + .returns({ + globalValue: ['/migrated/paths'], + }); +``` + +## 🧪 Step 7: Test Categories and Patterns + +### Configuration Tests + +- Test different setting combinations +- Test setting precedence (workspace > user > default) +- Test configuration errors and recovery + +### Data Flow Tests + +- Test how data moves through the system +- Test transformations (path resolution, filtering) +- Test state changes (migrations, updates) + +### Error Handling Tests + +- Test graceful degradation +- Test error logging +- Test fallback behaviors + +### Integration Tests + +- Test multiple features together +- Test real-world scenarios +- Test edge case combinations + +## 📊 Step 8: Review and Refine + +### Test Quality Checklist + +- [ ] **Clear naming** - test names describe the scenario and expected outcome +- [ ] **Good coverage** - main flows, edge cases, error scenarios +- [ ] **Resilient assertions** - won't break due to minor changes +- [ ] **Readable structure** - follows Mock → Run → Assert pattern +- [ ] **Isolated tests** - each test is independent +- [ ] **Fast execution** - tests run quickly with proper mocking + +### Common Anti-Patterns to Avoid + +- ❌ Testing implementation details instead of behavior +- ❌ Brittle assertions that break on cosmetic changes +- ❌ Order-dependent tests that fail due to processing changes +- ❌ Tests that don't clean up mocks properly +- ❌ Overly complex test setup that's hard to understand + +## 🧠 Agent Learnings + +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility (1) +- Use `runTests` tool for programmatic test execution rather than terminal commands for better integration and result parsing (1) +- Mock wrapper functions (e.g., `workspaceApis.getConfiguration()`) instead of VS Code APIs directly to avoid stubbing issues (2) +- Start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built (1) +- Use `sinon.match()` patterns for resilient assertions that don't break on minor output changes (2) +- Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing (1) +- When fixing mock environment creation, use `null` to truly omit properties rather than `undefined` (1) +- Always recompile TypeScript after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports (2) +- Create proxy abstraction functions for Node.js APIs like `cp.spawn` to enable clean testing - use function overloads to preserve Node.js's intelligent typing while making the functions mockable (1) +- When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet (1) +- When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing Task-related APIs (`Task`, `TaskScope`, `ShellExecution`, `TaskRevealKind`, `TaskPanelKind`) and namespace mocks (`tasks`) following the existing pattern of `mockedVSCode.X = vscodeMocks.vscMockExtHostedTypes.X` (1) +- Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., mockApi as PythonEnvironmentApi) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test (1) diff --git a/.github/prompts/add-telemetry.prompt.md b/.github/prompts/add-telemetry.prompt.md new file mode 100644 index 0000000..18c643d --- /dev/null +++ b/.github/prompts/add-telemetry.prompt.md @@ -0,0 +1,22 @@ +--- +mode: agent +--- + +If the user does not specify an event name or properties, pick an informative and descriptive name for the telemetry event based on the task or feature. Add properties as you see fit to collect the necessary information to achieve the telemetry goal, ensuring they are relevant and useful for diagnostics or analytics. + +When adding telemetry: + +- If the user wants to record when an action is started (such as a command invocation), place the telemetry call at the start of the handler or function. +- If the user wants to record successful completions or outcomes, place the telemetry call at the end of the action, after the operation has succeeded (and optionally, record errors or failures as well). + +Instructions to add a new telemetry event: + +1. Add a new event name to the `EventNames` enum in `src/common/telemetry/constants.ts`. +2. Add a corresponding entry to the `IEventNamePropertyMapping` interface in the same file, including a GDPR comment and the expected properties. +3. In the relevant code location, call `sendTelemetryEvent` with the new event name and required properties. Example: + ```typescript + sendTelemetryEvent(EventNames.YOUR_EVENT_NAME, undefined, { property: value }); + ``` +4. If the event is triggered by a command, ensure the call is placed at the start of the command handler. + +Expected output: The new event is tracked in telemetry and follows the GDPR and codebase conventions. diff --git a/.github/workflows/community-feedback-auto-comment.yml b/.github/workflows/community-feedback-auto-comment.yml new file mode 100644 index 0000000..f606148 --- /dev/null +++ b/.github/workflows/community-feedback-auto-comment.yml @@ -0,0 +1,28 @@ +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@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0 + 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@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + 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/info-needed-closer.yml b/.github/workflows/info-needed-closer.yml new file mode 100644 index 0000000..5d34581 --- /dev/null +++ b/.github/workflows/info-needed-closer.yml @@ -0,0 +1,33 @@ +name: Info-Needed Closer +on: + schedule: + - cron: 20 12 * * * # 5:20am Redmond + repository_dispatch: + types: [trigger-needs-more-info] + workflow_dispatch: + +permissions: + issues: write + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + persist-credentials: false + ref: stable + - name: Install Actions + run: npm install --production --prefix ./actions + - name: Run info-needed Closer + uses: ./actions/needs-more-info-closer + with: + token: ${{secrets.GITHUB_TOKEN}} + label: info-needed + closeDays: 14 + 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: 14 + 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." diff --git a/.github/workflows/issue-labels.yml b/.github/workflows/issue-labels.yml new file mode 100644 index 0000000..1199673 --- /dev/null +++ b/.github/workflows/issue-labels.yml @@ -0,0 +1,33 @@ +name: Issue labels + +on: + issues: + types: [opened, reopened] + +env: + TRIAGERS: '["karthiknadig","eleanorjboyd"]' + +permissions: + issues: write + +jobs: + # From https://github.com/marketplace/actions/github-script#apply-a-label-to-an-issue. + add-classify-label: + name: "Add 'triage-needed' and remove assignees" + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: "Add 'triage-needed' and remove assignees" + uses: ./actions/python-issue-labels + with: + triagers: ${{ env.TRIAGERS }} + token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 8aa6cc7..44bd661 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -8,8 +8,7 @@ on: - release* env: - NODE_VERSION: 18.17.1 - + NODE_VERSION: '20.18.1' jobs: build-vsix: @@ -24,3 +23,53 @@ jobs: uses: ./.github/actions/build-vsix with: node_version: ${{ env.NODE_VERSION }} + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Run Linter + run: npm run lint + + ts-unit-tests: + name: TypeScript Unit Tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Compile Tests + run: npm run pretest + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Run Tests + run: npm run unittest diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml new file mode 100644 index 0000000..0f01dde --- /dev/null +++ b/.github/workflows/pr-labels.yml @@ -0,0 +1,24 @@ +name: 'PR labels' +on: + pull_request: + types: + - 'opened' + - 'reopened' + - 'labeled' + - 'unlabeled' + - 'synchronize' + +jobs: + add-pr-label: + name: 'Ensure Required Labels' + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: 'PR impact specified' + uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5.5.0 + with: + mode: exactly + count: 1 + labels: 'bug, debt, feature-request, no-changelog' diff --git a/.github/workflows/push-check.yml b/.github/workflows/push-check.yml index e8dd70e..1f3ae75 100644 --- a/.github/workflows/push-check.yml +++ b/.github/workflows/push-check.yml @@ -9,7 +9,7 @@ on: - 'release-*' env: - NODE_VERSION: 18.17.1 + NODE_VERSION: '20.18.1' jobs: build-vsix: @@ -23,4 +23,54 @@ jobs: - name: Build VSIX uses: ./.github/actions/build-vsix with: - node_version: ${{ env.NODE_VERSION }} \ No newline at end of file + node_version: ${{ env.NODE_VERSION }} + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Run Linter + run: npm run lint + + ts-unit-tests: + name: TypeScript Unit Tests + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install Dependencies + run: npm ci + + - name: Compile Tests + run: npm run pretest + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Run Tests + run: npm run unittest diff --git a/.github/workflows/remove-needs-labels.yml b/.github/workflows/remove-needs-labels.yml new file mode 100644 index 0000000..2435252 --- /dev/null +++ b/.github/workflows/remove-needs-labels.yml @@ -0,0 +1,20 @@ +name: 'Remove Needs Label' +on: + issues: + types: [closed] + +jobs: + classify: + name: 'Remove needs labels on issue closing' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: 'Removes needs labels on issue close' + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 + with: + labels: | + needs PR + needs spike + needs community feedback + needs proposal diff --git a/.github/workflows/test_plan_item_validator.yml b/.github/workflows/test_plan_item_validator.yml new file mode 100644 index 0000000..91e8948 --- /dev/null +++ b/.github/workflows/test_plan_item_validator.yml @@ -0,0 +1,30 @@ +name: Test Plan Item Validator +on: + issues: + types: [edited, labeled] + +permissions: + issues: write + +jobs: + main: + runs-on: ubuntu-latest + 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@v4 + with: + repository: 'microsoft/vscode-github-triage-actions' + path: ./actions + persist-credentials: false + ref: stable + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Run Test Plan Item Validator + uses: ./actions/test-plan-item-validator + with: + label: testplan-item + invalidLabel: invalid-testplan-item + comment: Invalid test plan item. See errors below and the [test plan item spec](https://github.com/microsoft/vscode/wiki/Writing-Test-Plan-Items) for more information. This comment will go away when the issues are resolved. diff --git a/.github/workflows/triage-info-needed.yml b/.github/workflows/triage-info-needed.yml new file mode 100644 index 0000000..1fd316f --- /dev/null +++ b/.github/workflows/triage-info-needed.yml @@ -0,0 +1,57 @@ +name: Triage "info-needed" label + +on: + issue_comment: + types: [created] + +env: + TRIAGERS: '["karthiknadig","eleanorjboyd","anthonykim1"]' + +jobs: + add_label: + if: contains(github.event.issue.labels.*.name, 'triage-needed') && !contains(github.event.issue.labels.*.name, 'info-needed') + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + persist-credentials: false + + - name: Install Actions + run: npm install --production --prefix ./actions + + - 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 + permissions: + issues: write + steps: + - name: Checkout Actions + uses: actions/checkout@v4 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: stable + path: ./actions + persist-credentials: false + + - name: Install Actions + run: npm install --production --prefix ./actions + + - name: Remove "info-needed" label + uses: ./actions/python-triage-info-needed + with: + triagers: ${{ env.TRIAGERS }} + action: 'remove' + token: ${{secrets.GITHUB_TOKEN}} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..d4b7699 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +20.18.1 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 57dbdae..f36bb36 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,5 +1,5 @@ { - // See http://go.microsoft.com/fwlink/?LinkId=827846 - // for the documentation about the extensions.json format - "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher", "esbenp.prettier-vscode"] } diff --git a/.vscode/launch.json b/.vscode/launch.json index d4df9e9..af8e230 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,33 +3,45 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/dist/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js", - "${workspaceFolder}/dist/**/*.js" - ], - "preLaunchTask": "tasks: watch-tests" - } - ] + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Unit Tests", + "type": "node", + "request": "launch", + "args": [ + "-u=tdd", + "--timeout=180000", + "--colors", + "--recursive", + //"--grep", "", + "--require=out/test/unittests.js", + "./out/test/**/*.unit.test.js" + ], + "internalConsoleOptions": "openOnSessionStart", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "skipFiles": ["/**"], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "npm: watch-tests" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/" + ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 8d4822a..5558607 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,9 +10,22 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", + "editor.formatOnSave": true, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } }, - "prettier.tabWidth": 4 -} \ No newline at end of file + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "diffEditor.ignoreTrimWhitespace": false, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "prettier.tabWidth": 4, + "python-envs.defaultEnvManager": "ms-python.python:venv", + "python-envs.pythonProjects": [], + "git.branchRandomName.enable": true +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c2ab68a..1719917 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,40 +1,54 @@ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$ts-webpack-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "type": "npm", - "script": "watch-tests", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never", - "group": "watchers" - }, - "group": "build" - }, - { - "label": "tasks: watch-tests", - "dependsOn": [ - "npm: watch", - "npm: watch-tests" - ], - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "watch-tests", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + } + }, + { + "label": "tasks: build", + "dependsOn": ["npm: watch", "npm: watch-tests"], + "problemMatcher": [], + "presentation": { + "reveal": "never", + "group": "watchers" + } + }, + { + "type": "npm", + "script": "unittest", + "dependsOn": ["tasks: watch-tests"], + "problemMatcher": "$tsc", + "presentation": { + "reveal": "never", + "group": "test" + }, + "group": { + "kind": "test", + "isDefault": false + } + } + ] } diff --git a/.vscodeignore b/.vscodeignore index e95e16b..14a7a5a 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -4,6 +4,7 @@ out/** node_modules/** src/** .gitignore +.vscode-test.mjs .yarnrc webpack.config.js vsc-extension-quickstart.md @@ -13,4 +14,5 @@ vsc-extension-quickstart.md **/*.ts .nox/ .venv/ -**/__pycache__/ \ No newline at end of file +**/__pycache__/ +examples/** diff --git a/README.md b/README.md index fe2e075..c73f308 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,324 @@ -# Python Environments and Package Manager (preview) +# Python Environments (preview) + +> **Note:** The Python Environments icon may no longer appear in the Activity Bar due to the ongoing rollout of the Python Environments extension. To restore the extension, add `"python.useEnvironmentsExtension": true` to your User settings. This setting is temporarily necessary until the rollout is complete! ## Overview -Python Environments and Package Manager is a VS Code extension that helps users manage their Python environments and package management. It is a preview extension and the APIs and features are subject to change as the project evolves. +The Python Environments extension for VS Code helps you manage Python environments and packages using your preferred environment manager, backed by its extensible APIs. This extension provides unique support for specifying environments for specific files, entire Python folders, or projects, including multi-root and mono-repo scenarios. The core feature set includes: + +- 🌐 Create, delete, and manage environments +- 📦 Install and uninstall packages within the selected environment +- ✅ Create activated terminals +- 🖌️ Add and create new Python projects + +> **Note:** This extension is in preview, and its APIs and features are subject to change as the project evolves. + +> **Important:** This extension requires version `2024.23`, or later, of the Python extension (`ms-python.python`). ## Features +The "Python Projects" fold shows you all of the projects that are currently in your workspace and their selected environments. From this view you can add more files or folders as projects, select a new environment for your project, and manage your selected environments. + +The "Environment Managers" fold shows you all of the environment managers that are available on your machine with all related environments nested below. From this view, you can create new environments, delete old environments, and manage packages. + + + ### Environment Management -This extension provides an environment view for the user to manage their Python environments. The user can create, delete, and switch between environments. The user can also install and uninstall packages in the current environment. This extension provides APIs for extension developers to contribute environment managers. +The Python Environments panel provides an interface to create, delete and manage environments. + + + +To simplify the environment creation process, you can use "Quick Create" to automatically create a new virtual environment using: + +- Your default environment manager (e.g., `venv`) +- The latest Python version +- Workspace dependencies + +For more control, you can create a custom environment where you can specify Python version, environment name, packages to be installed, and more! -The extension by uses `venv` as the default environment manager. You can change this by setting the `python-envs.defaultEnvManager` setting to a different environment manager. Following are the out of the box environment managers: +The following environment managers are supported out of the box: -|Id| name |Description| -|---|----|--| -|ms-python.python:venv| `venv` |The default environment manager. It is a built-in environment manager provided by the Python standard library.| -|ms-python.python:system| System Installed Python | These are python installs on your system. Installed either with your OS, or from python.org, or any other OS package manager | -|ms-python.python:conda| `conda` |The conda environment manager. It is a popular environment manager for Python.| +| Id | Name | Description | +| ----------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ms-python.python:venv | `venv` | Built-in environment manager provided by the Python standard library. Supports creating environments (interactive and quick create) and finding existing environments. | +| ms-python.python:system | System Installed Python | Global Python installs on your system, typically installed with your OS, from [python.org](https://www.python.org/), or any other OS package manager. | +| ms-python.python:conda | `conda` | The [conda](https://conda.org) environment manager, as provided by conda distributions like [Anaconda Distribution](https://docs.anaconda.com/anaconda/) or [conda-forge](https://conda-forge.org/download/). Supports creating environments (interactive and quick create) and finding existing environments. | +| ms-python.python:pyenv | `pyenv` | The [pyenv](https://github.com/pyenv/pyenv) environment manager, used to manage multiple Python versions. Supports finding existing environments. | +| ms-python.python:poetry | `poetry` | The [poetry](https://python-poetry.org/) environment manager, used for dependency management and packaging in Python projects. Supports finding existing environments. | +| ms-python.python:pipenv | `pipenv` | The [pipenv](https://pipenv.pypa.io/en/latest/) environment manager, used for managing Python dependencies and environments. Only supports finding existing environments. | +#### Supported Actions by Environment Manager + +| Environment Manager | Find Environments | Create | Quick Create | +| ------------------- | ----------------- | ------ | ------------ | +| venv | ✅ | ✅ | ✅ | +| conda | ✅ | ✅ | ✅ | +| pyenv | ✅ | | | +| poetry | ✅ | | | +| system | ✅ | | | +| pipenv | ✅ | | | + +**Legend:** + +- **Create**: Ability to create new environments interactively. +- **Quick Create**: Ability to create environments with minimal user input. +- **Find Environments**: Ability to discover and list existing environments. + +Environment managers are responsible for specifying which package manager will be used by default to install and manage Python packages within the environment (`venv` uses `pip` by default). This ensures that packages are managed consistently according to the preferred tools and settings of the chosen environment manager. ### Package Management -This extension provides a package view for the user to manage their Python packages. The user can install and uninstall packages in the any environment. This extension provides APIs for extension developers to contribute package managers. +The extension also provides an interface to install and uninstall Python packages, and provides APIs for extension developers to contribute package managers of their choice. + +The extension uses `pip` as the default package manager, but you can use the package manager of your choice using the `python-envs.defaultPackageManager` setting. The following are package managers supported out of the box: + +| Id | Name | Description | +| ---------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ms-python.python:pip | `pip` | Pip acts as the default package manager and it's typically built-in to Python. | +| ms-python.python:conda | `conda` | The [conda](https://conda.org) package manager, as provided by conda distributions like [Anaconda Distribution](https://docs.anaconda.com/anaconda/) or [conda-forge](https://conda-forge.org/download/). | + +#### Default Package Manager by Environment Manager + +| Environment Manager | Default Package Manager | +| ------------------- | ----------------------- | +| venv | pip | +| conda | conda | +| pyenv | pip | +| poetry | poetry | +| system | pip | +| pipenv | pip | + +### Project Management + +A "Python Project" is any file or folder that contains runnable Python code and needs its own environment. With the Python Environments extension, you can add files and folders as projects in your workspace and assign individual environments to them allowing you to run various projects more seamlessly. + +Projects can be added via the Python Environments pane or in the File Explorer by right-clicking on the folder/file and selecting the "Add as Python Project" menu item. + +There are a few ways to add a Python Project from the Python Environments panel: + +| Name | Description | +| ------------ | ---------------------------------------------------------------------- | +| Add Existing | Allows you to add an existing folder from the file explorer. | +| Auto find | Searches for folders that contain `pyproject.toml` or `setup.py` files | +| Create New | Creates a new project from a template. | + +#### Create New Project from Template + +The **Python Envs: Create New Project from Template** command simplifies the process of starting a new Python project by scaffolding it for you. Whether in a new workspace or an existing one, this command configures the environment and boilerplate file structure, so you don’t have to worry about the initial setup, and only the code you want to write. There are currently two project types supported: + +- Package: A structured Python package with files like `__init__.py` and setup configurations. +- Script: A simple project for standalone Python scripts, ideal for quick tasks or just to get you started. + +## Command Reference + +All commands can be accessed via the Command Palette (`ctrl/cmd + Shift + P`): + +| Name | Description | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| Create Environment | Create a virtual environment using your preferred environment manager preconfigured with "Quick Create" or configured to your choices. | +| Manage Packages | Install and uninstall packages in a given Python environment. | +| Activate Environment in Current Terminal | Activates the currently opened terminal with a particular environment. | +| Deactivate Environment in Current Terminal | Deactivates environment in currently opened terminal. | +| Run as Task | Runs Python module as a task. | +| Create New Project from Template | Creates scaffolded project with virtual environments based on a template. | + +## Settings Reference + +| Setting (python-envs.) | Default | Description | +| --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| defaultEnvManager | `"ms-python.python:venv"` | The default environment manager used for creating and managing environments. | +| defaultPackageManager | `"ms-python.python:pip"` | The default package manager to use for installing and managing packages. This is often dictated by the default environment manager but can be customized. | +| pythonProjects | `[]` | A list of Python workspaces, specified by the path, in which you can set particular environment and package managers. You can set information for a workspace as `[{"path": "/path/to/workspace", "envManager": "ms-python.python:venv", "packageManager": "ms-python.python:pip"]}`. | +| terminal.showActivateButton | `false` | (experimental) Show a button in the terminal to activate/deactivate the current environment for the terminal. This button is only shown if the active terminal is associated with a project that has an activatable environment. | +| python-envs.terminal.autoActivationType | `command` | Specifies how the extension can activate an environment in a terminal. Utilizing Shell Startup requires changes to the shell script file and is only enabled for the following shells: zsh, fsh, pwsh, bash, cmd. When set to `command`, any shell can be activated. This setting applies only when terminals are created, so you will need to restart your terminals for it to take effect. To revert changes made during shellStartup, run `Python Envs: Revert Shell Startup Script Changes`. | + +## Extensibility + +The Python Environments extension was built to provide a cohesive and user friendly experience with `venv` as the default. However, the extension is built with extensibility in mind so that any environment manager could build an extension using the supported APIs to plug-in and provide a seamless and incorporated experience for their users in VS Code. + +### API Reference (proposed) + +See [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/api.ts) for the full list of Extension APIs. + +To consume these APIs you can look at the example here: [API Consumption Examples](https://github.com/microsoft/vscode-python-environments/blob/main/examples/README.md) + +### Callable Commands + +The extension provides a set of callable commands that can be used to interact with the environment and package managers. These commands can be invoked from other extensions or from the command palette. + +#### `python-envs.createAny` + +Create a new environment using any of the available environment managers. This command will prompt the user to select the environment manager to use. Following options are available on this command: -The extension by uses `pip` as the default package manager. You can change this by setting the `python-envs.defaultPackageManager` setting to a different package manager. Following are the out of the box package managers: +```typescript +{ + /** + * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. + */ + showBackButton?: boolean; -|Id| name |Description| -|---|----|--| -|ms-python.python:pip| `pip` |The default package manager. It is a built-in package manager provided by the Python standard library.| -|ms-python.python:conda| `conda` |The conda package manager. It is a popular package manager for Python.| + /** + * Default `true`. If `true`, the environment after creation will be selected. + */ + selectEnvironment?: boolean; -## API Reference + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} +``` -See the `src\api.ts` for the full list of APIs. +usage: `await vscode.commands.executeCommand('python-envs.createAny', options);` + +# Experimental Features + +## Shell Startup Activation + +The Python Environments extension supports shell startup activation for environments. This feature allows you to automatically activate a Python environment when you open a terminal in VS Code. The activation is done by modifying the shell's startup script, which is supported for the following shells: + +- **Bash**: `~/.bashrc` +- **Zsh**: `~/.zshrc` (or `$ZDOTDIR/.zshrc` if `ZDOTDIR` is set) +- **Fish**: `~/.config/fish/config.fish` +- **PowerShell**: + - (Mac/Linux):`~/.config/powershell/profile.ps1` + - (Windows): `~\Documents\PowerShell\Microsoft.PowerShell_profile.ps1` +- **CMD**: `~/.cmdrc/cmd_startup.bat` + +If at any time you would like to revert the changes made to the shell's script, you can do so by running `Python Envs: Revert Shell Startup Script Changes` via the Command Palette. + +### CMD + +1. Add or update `HKCU\\Software\\Microsoft\\Command Processor` `AutoRun` string value to use a command script. +2. A command script is added to `%USERPROFILE%\.cmdrc\cmd_startup.bat` +3. A script named `vscode-python.bat` is added to `%USERPROFILE%\.cmdrc` and called from `cmd_startup.bat` + +contents of `cmd_startup.bat` + +```bat +:: >>> vscode python +if "%TERM_PROGRAM%"=="vscode" ( + if not defined VSCODE_PYTHON_AUTOACTIVATE_GUARD ( + set "VSCODE_PYTHON_AUTOACTIVATE_GUARD=1" + if exist "%USERPROFILE%\.cmdrc\vscode-python.bat" call "%USERPROFILE%\.cmdrc\vscode-python.bat" + ) +) +:: <<< vscode python +``` + +contents of `vscode-python.bat` + +```bat +:: >>> vscode python +:: version: 0.1.0 +if defined VSCODE_CMD_ACTIVATE ( + call %VSCODE_CMD_ACTIVATE% +) +:: <<< vscode python +``` + +### Powershell/pwsh + +1. Runs `powershell -Command $profile` to get the profile location +2. If it does not exist creates it. +3. Adds following code to the shell profile script: + +```powershell +#region vscode python +if ($null -ne $env:VSCODE_PWSH_ACTIVATE) { + Invoke-Expression $env:VSCODE_PWSH_ACTIVATE +} +#endregion vscode python + +``` + +### sh/bash/gitbash + +1. Adds or creates `~/.bashrc` +2. Updates it with following code: + +```bash +# >>> vscode python +# version: 0.1.0 +if [ -n "$VSCODE_BASH_ACTIVATE" ] && [ "$TERM_PROGRAM" = "vscode" ]; then + eval "$VSCODE_BASH_ACTIVATE" || true +fi +# <<< vscode python +``` + +### zsh + +1. Adds or creates `~/.zshrc` (or `$ZDOTDIR/.zshrc` if `ZDOTDIR` is set) +2. Updates it with following code: + +```zsh +# >>> vscode python +# version: 0.1.0 +if [ -n "$VSCODE_BASH_ACTIVATE" ] && [ "$TERM_PROGRAM" = "vscode" ]; then + eval "$VSCODE_BASH_ACTIVATE" || true +fi +# <<< vscode python +``` + +### fish + +1. Adds or creates `~/.config/fish/config.fish` +2. Updates it with following code: + +```fish +# >>> vscode python +# version: 0.1.0 +if test "$TERM_PROGRAM" = "vscode"; and set -q VSCODE_FISH_ACTIVATE + eval $VSCODE_FISH_ACTIVATE +end +# <<< vscode python +``` + +## Extension Dependency + +This section provides an overview of how the Python extension interacts with the Python Environments extension and other tool-specific extensions. The Python Environments extension allows users to create, manage, and remove Python environments and packages. It also provides an API that other extensions can use to support environment management or consume it for running Python tools or projects. + +Tools that may rely on these APIs in their own extensions include: + +- **Debuggers** (e.g., `debugpy`) +- **Linters** (e.g., Pylint, Flake8, Mypy) +- **Formatters** (e.g., Black, autopep8) +- **Language Server extensions** (e.g., Pylance, Jedi) +- **Environment and Package Manager extensions** (e.g., Pixi, Conda, Hatch) + +### API Dependency + +The relationship between these extensions can be represented as follows: + + + +Users who do not need to execute code or work in **Virtual Workspaces** can use the Python extension to access language features like hover, completion, and go-to definition. However, executing code (e.g., running a debugger, linter, or formatter), creating/modifying environments, or managing packages requires the Python Environments extension to enable these functionalities. + +### Trust Relationship Between Python and Python Environments Extensions + +VS Code supports trust management, allowing extensions to function in either **trusted** or **untrusted** scenarios. Code execution and tools that can modify the user’s environment are typically unavailable in untrusted scenarios. + +The relationship is illustrated below: + + + +In **trusted mode**, the Python Environments extension supports tasks like managing environments, installing/removing packages, and running tools. In **untrusted mode**, functionality is limited to language features, ensuring a secure and restricted environment. ## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +the rights to use your contribution. For details, visit . When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions @@ -48,10 +328,23 @@ This project has adopted the [Microsoft Open Source Code of Conduct](https://ope For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +## Questions, issues, feature requests, and contributions + +- If you have a question about how to accomplish something with the extension, please [ask on our Discussions page](https://github.com/microsoft/vscode-python/discussions/categories/q-a). +- If you come across a problem with the extension, please [file an issue](https://github.com/microsoft/vscode-python). +- Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md) for more details. +- Any and all feedback is appreciated and welcome! + - If someone has already [filed an issue](https://github.com/Microsoft/vscode-python) that encompasses your feedback, please leave a 👍/👎 reaction on the issue. + - Otherwise please start a [new discussion](https://github.com/microsoft/vscode-python/discussions/categories/ideas). +- If you're interested in the development of the extension, you can read about our [development process](https://github.com/Microsoft/vscode-python/blob/main/CONTRIBUTING.md#development-process). + +## Data and telemetry + +The Microsoft Python Extension for Visual Studio Code collects usage data and sends it to Microsoft to help improve our products and services. Read our [privacy statement](https://privacy.microsoft.com/privacystatement) to learn more. This extension respects the `telemetry.enableTelemetry` setting which you can learn more about at . + ## Trademarks -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. diff --git a/build/.mocha.unittests.json b/build/.mocha.unittests.json new file mode 100644 index 0000000..028d479 --- /dev/null +++ b/build/.mocha.unittests.json @@ -0,0 +1,8 @@ +{ + "spec": "./out/test/**/*.unit.test.js", + "require": ["source-map-support/register", "out/test/unittests.js"], + "ui": "tdd", + "recursive": true, + "colors": true, + "timeout": 180000 +} diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml new file mode 100644 index 0000000..bb97b9b --- /dev/null +++ b/build/azure-pipeline.pre-release.yml @@ -0,0 +1,146 @@ +# Run on a schedule +trigger: none +pr: none + +schedules: + - cron: '0 10 * * 1-5' # 10AM UTC (2AM PDT) MON-FRI (VS Code Pre-release builds at 9PM PDT) + displayName: Nightly Pre-Release Schedule + always: false # only run if there are source code changes + branches: + include: + - main + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + 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 }} + ghCreateTag: false + standardizedVersioning: true + l10nSourcePaths: ./src + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '20.18.1' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: npm ci + displayName: Install NPM dependencies + + - script: python ./build/update_package_json.py + displayName: Update telemetry in package.json + + - script: python ./build/update_ext_version.py --for-publishing + displayName: Update build number + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 591 + buildVersionToDownload: 'latest' + branchName: 'refs/heads/main' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -l ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -l ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: npm run package + displayName: Build extension + + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml new file mode 100644 index 0000000..348a8d9 --- /dev/null +++ b/build/azure-pipeline.stable.yml @@ -0,0 +1,135 @@ +trigger: none +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/extension/stable.yml@templates + parameters: + l10nSourcePaths: ./src + publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: true + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '20.18.1' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.8' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: npm ci + displayName: Install NPM dependencies + + - script: python ./build/update_package_json.py + displayName: Update telemetry in package.json + + - script: python ./build/update_ext_version.py --release --for-publishing + displayName: Update build number + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 593 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/release/2025.10' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: npm run package + displayName: Build extension + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/build/test_update_ext_version.py b/build/test_update_ext_version.py new file mode 100644 index 0000000..1a2fdb0 --- /dev/null +++ b/build/test_update_ext_version.py @@ -0,0 +1,104 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json + +import freezegun +import pytest +import update_ext_version + +TEST_DATETIME = "2022-03-14 01:23:45" + +# The build ID is calculated via: +# "1" + datetime.datetime.strptime(TEST_DATETIME,"%Y-%m-%d %H:%M:%S").strftime('%j%H%M') +EXPECTED_BUILD_ID = "10730123" + + +def create_package_json(directory, version): + """Create `package.json` in `directory` with a specified version of `version`.""" + package_json = directory / "package.json" + package_json.write_text(json.dumps({"version": version}), encoding="utf-8") + return package_json + + +def run_test(tmp_path, version, args, expected): + package_json = create_package_json(tmp_path, version) + update_ext_version.main(package_json, args) + package = json.loads(package_json.read_text(encoding="utf-8")) + assert expected == update_ext_version.parse_version(package["version"]) + + +@pytest.mark.parametrize( + "version, args", + [ + ("1.0.0-rc", []), + ("1.1.0-rc", ["--release"]), + ("1.0.0-rc", ["--release", "--build-id", "-1"]), + ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "-1"]), + ("1.0.0-rc", ["--release", "--for-publishing", "--build-id", "999999999999"]), + ("1.1.0-rc", ["--build-id", "-1"]), + ("1.1.0-rc", ["--for-publishing", "--build-id", "-1"]), + ("1.1.0-rc", ["--for-publishing", "--build-id", "999999999999"]), + ], +) +def test_invalid_args(tmp_path, version, args): + with pytest.raises(ValueError): + run_test(tmp_path, version, args, None) + + +@pytest.mark.parametrize( + "version, args, expected", + [ + ("1.1.0-rc", ["--build-id", "12345"], ("1", "1", "12345", "rc")), + ("1.0.0-rc", ["--release", "--build-id", "12345"], ("1", "0", "12345", "")), + ( + "1.1.0-rc", + ["--for-publishing", "--build-id", "12345"], + ("1", "1", "12345", ""), + ), + ( + "1.0.0-rc", + ["--release", "--for-publishing", "--build-id", "12345"], + ("1", "0", "12345", ""), + ), + ( + "1.0.0-rc", + ["--release", "--build-id", "999999999999"], + ("1", "0", "999999999999", ""), + ), + ( + "1.1.0-rc", + ["--build-id", "999999999999"], + ("1", "1", "999999999999", "rc"), + ), + ("1.1.0-rc", [], ("1", "1", EXPECTED_BUILD_ID, "rc")), + ( + "1.0.0-rc", + ["--release"], + ("1", "0", "0", ""), + ), + ( + "1.1.0-rc", + ["--for-publishing"], + ("1", "1", EXPECTED_BUILD_ID, ""), + ), + ( + "1.0.0-rc", + ["--release", "--for-publishing"], + ("1", "0", "0", ""), + ), + ( + "1.0.0-rc", + ["--release"], + ("1", "0", "0", ""), + ), + ( + "1.1.0-rc", + [], + ("1", "1", EXPECTED_BUILD_ID, "rc"), + ), + ], +) +@freezegun.freeze_time("2022-03-14 01:23:45") +def test_update_ext_version(tmp_path, version, args, expected): + run_test(tmp_path, version, args, expected) diff --git a/build/update_ext_version.py b/build/update_ext_version.py new file mode 100644 index 0000000..b284163 --- /dev/null +++ b/build/update_ext_version.py @@ -0,0 +1,106 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import datetime +import json +import pathlib +import sys +from typing import Sequence, Tuple, Union + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def build_arg_parse() -> argparse.ArgumentParser: + """Builds the arguments parser.""" + parser = argparse.ArgumentParser( + description="This script updates the python extension micro version based on the release or pre-release channel." + ) + parser.add_argument( + "--release", + action="store_true", + help="Treats the current build as a release build.", + ) + parser.add_argument( + "--build-id", + action="store", + type=int, + default=None, + help="If present, will be used as a micro version.", + required=False, + ) + parser.add_argument( + "--for-publishing", + action="store_true", + help="Removes `-dev` or `-rc` suffix.", + ) + return parser + + +def is_even(v: Union[int, str]) -> bool: + """Returns True if `v` is even.""" + return not int(v) % 2 + + +def micro_build_number() -> str: + """Generates the micro build number. + The format is `1`. + """ + return f"1{datetime.datetime.now(tz=datetime.timezone.utc).strftime('%j%H%M')}" + + +def parse_version(version: str) -> Tuple[str, str, str, str]: + """Parse a version string into a tuple of version parts.""" + major, minor, parts = version.split(".", maxsplit=2) + try: + micro, suffix = parts.split("-", maxsplit=1) + except ValueError: + micro = parts + suffix = "" + return major, minor, micro, suffix + + +def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: + parser = build_arg_parse() + args = parser.parse_args(argv) + + package = json.loads(package_json.read_text(encoding="utf-8")) + + major, minor, micro, suffix = parse_version(package["version"]) + + if args.release and not is_even(minor): + raise ValueError( + f"Release version should have EVEN numbered minor version: {package['version']}" + ) + elif not args.release and is_even(minor): + raise ValueError( + f"Pre-Release version should have ODD numbered minor version: {package['version']}" + ) + + print(f"Updating build FROM: {package['version']}") + if args.build_id: + # If build id is provided it should fall within the 0-INT32 max range + # that the max allowed value for publishing to the Marketplace. + if args.build_id < 0 or (args.for_publishing and args.build_id > ((2**32) - 1)): + raise ValueError(f"Build ID must be within [0, {(2**32) - 1}]") + + package["version"] = ".".join((major, minor, str(args.build_id))) + elif args.release: + package["version"] = ".".join((major, minor, micro)) + else: + # micro version only updated for pre-release. + package["version"] = ".".join((major, minor, micro_build_number())) + + if not args.for_publishing and not args.release and len(suffix): + package["version"] += "-" + suffix + print(f"Updating build TO: {package['version']}") + + # 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/build/update_package_json.py b/build/update_package_json.py new file mode 100644 index 0000000..d5b45dc --- /dev/null +++ b/build/update_package_json.py @@ -0,0 +1,19 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + + +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(pathlib.Path(__file__).parent.parent / "package.json") diff --git a/builds/validate_packages.py b/build/validate_packages.py similarity index 59% rename from builds/validate_packages.py rename to build/validate_packages.py index 9cef608..f3e252f 100644 --- a/builds/validate_packages.py +++ b/build/validate_packages.py @@ -1,8 +1,13 @@ +# Usage: +# Windows: type package_list.txt | python validate_packages.py > valid_packages.txt +# Linux/Mac: cat package_list.txt | python validate_packages.py > valid_packages.txt + import json -import pathlib +import sys import urllib import urllib.request as url_lib + def _get_pypi_package_data(package_name): json_uri = "https://pypi.org/pypi/{0}/json".format(package_name) # Response format: https://warehouse.readthedocs.io/api-reference/json/#project @@ -10,22 +15,20 @@ def _get_pypi_package_data(package_name): with url_lib.urlopen(json_uri) as response: return json.loads(response.read()) -packages = (pathlib.Path(__file__).parent.parent / "files" / "pip_packages.txt").read_text(encoding="utf-8").splitlines() -valid_packages = [] - def validate_package(package): try: data = _get_pypi_package_data(package) num_versions = len(data["releases"]) - return num_versions > 1 + return num_versions > 1 except urllib.error.HTTPError: return False -for pkg in packages: - if(validate_package(pkg)): - print(pkg) - valid_packages.append(pkg) - -(pathlib.Path(__file__).parent / "valid_pip_packages.txt").write_text('\n'.join(valid_packages), encoding="utf-8") \ No newline at end of file +if __name__ == "__main__": + packages = sys.stdin.read().splitlines() + valid_packages = [] + for pkg in packages: + if validate_package(pkg): + print(pkg) + valid_packages.append(pkg) diff --git a/docs/projects-api-reference.md b/docs/projects-api-reference.md new file mode 100644 index 0000000..85531f4 --- /dev/null +++ b/docs/projects-api-reference.md @@ -0,0 +1,75 @@ +# Python Projects API Reference + +## Modifying Projects +This is how the projects API is designed with the different parts of the project flow. Here `getPythonProjects` is used as an example function but behavior will mirror other getter and setter functions exposed in the API. + +1. **API Call:** Extensions can calls `getPythonProjects` on [`PythonEnvironmentApi`](../src/api.ts). +2. **API Implementation:** [`PythonEnvironmentApiImpl`](../src/features/pythonApi.ts) delegates to its internal project manager. +3. **Internal API:** The project manager is typed as [`PythonProjectManager`](../src/internal.api.ts). +4. **Concrete Implementation:** [`PythonProjectManagerImpl`](../src/features/projectManager.ts) implements the actual logic. +5. **Data Model:** Returns an array of [`PythonProject`](../src/api.ts) objects. + +## Project Creators +This is how creating projects work with the API as it uses a method of registering +external or internal project creators and maintaining project states internally in +just this extension. + +- **Project Creators:** Any extension can implement and register a project creator by conforming to the [`PythonProjectCreator`](../src/api.ts) interface. Each creator provides a `create` method that returns one or more new projects (or their URIs). The create method is responsible for add the new projects to the project manager. +- **Registration:** Project creators are registered with the API, making them discoverable and usable by the extension or other consumers. +- **Integration:** Once a project is created, it is added to the internal project manager (`PythonProjectManagerImpl` in [`src/features/projectManager.ts`](../src/features/projectManager.ts)), which updates the set of known projects and persists settings if necessary. + +### What an Extension Must Do + +1. **Implement the Creator Interface:** + - Create a class that implements the [`PythonProjectCreator`](../src/api.ts) interface. + - Provide a unique `name`, a user-friendly `displayName`, and a `create` method that returns one or more `PythonProject` objects or URIs. + +2. **Register the Creator:** + - Register the creator with the main API (usually via a registration method exposed by this extension’s API surface). + - This makes the creator discoverable and usable by the extension and other consumers. + +3. **Add Projects Directly:** + - If your creator directly creates `PythonProject` objects, you MUST call the internal project manager’s `add` method during your create function to add projects as ones in the workspace. + + +### Responsibilities Table + +| Step | External Extension’s Responsibility | Internal Python-Envs-Ext Responsibility | +| ------------------------------------------ | :---------------------------------: | :-------------------------------------: | +| Implement `PythonProjectCreator` interface | ☑️ | | +| Register the creator | ☑️ | | +| Provide `create` method | ☑️ | | +| Add projects to project manager | ☑️ | | +| Update project settings | | ☑️ | +| Track and list creators | | ☑️ | +| Invoke creator and handle results | | ☑️ | + + +### Example Implementation: [`ExistingProjects`](../src/features/creators/existingProjects.ts) + +The [`ExistingProjects`](../src/features/creators/existingProjects.ts) class is an example of a project creator. It allows users to select files or folders from the workspace and creates new `PythonProject` instances for them. After creation, these projects are added to the internal project manager: + +create function implementation abbreviated: +```typescript +async create( + _options?: PythonProjectCreatorOptions, + ): Promise { +const projects = resultsInWorkspace.map( + (uri) => new PythonProjectsImpl(path.basename(uri.fsPath), uri), + ) as PythonProject[]; +this.pm.add(projects); +return projects; + } +``` + +creator registration (usually on extension activation): +``` + projectCreators.registerPythonProjectCreator(new ExistingProjects(projectManager)), + +``` + +- **Implements:** [`PythonProjectCreator`](../src/api.ts) +- **Adds projects to:** `PythonProjectManager` (see below) + + + diff --git a/eslint.config.mjs b/eslint.config.mjs index d5c0b53..cfa27fd 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -19,10 +19,22 @@ export default [{ selector: "import", format: ["camelCase", "PascalCase"], }], - + "@typescript-eslint/no-unused-vars": [ + "error", + { + "args": "all", + "argsIgnorePattern": "^_", + "caughtErrors": "all", + "caughtErrorsIgnorePattern": "^_", + "destructuredArrayIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "ignoreRestSiblings": true + } + ], curly: "warn", eqeqeq: "warn", "no-throw-literal": "warn", semi: "warn", + "@typescript-eslint/no-explicit-any": "error", }, }]; \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9a55d81 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,87 @@ +## Requirements + +1. `node` >= 20.18.1 +2. `npm` >= 10.9.0 +3. `yo` >= 5.0.0 (installed via `npm install -g yo`) +4. `generator-code` >= 1.11.4 (installed via `npm install -g generator-code`) + +## Create your extension + +### Scaffolding + +Run `yo code` in your terminal and follow the instructions to create a new extension. The following were the choices made for this example: + +``` +> yo code +? What type of extension do you want to create? New Extension (TypeScript) +? What's the name of your extension? Sample1 +? What's the identifier of your extension? sample1 +? What's the description of your extension? A sample environment manager +? Initialize a git repository? Yes +? Which bundler to use? webpack +? Which package manager to use? npm +``` + +Follow the generator's additional instructions to install the required dependencies and build your extension. + +### Update extension dependency + +Add the following dependency to your extension `package.json` file: + +```json + "extensionDependencies": [ + "ms-python.vscode-python-envs" + ], +``` + +### Set up the Python Envs API + +The Python environments API is available via the extension export. First, add the following file to your extension [api.ts](https://github.com/microsoft/vscode-python-environments/blob/main/src/api.ts). You can rename the file as you see fit for your extension. + +Add a `pythonEnvsApi.ts` file to get the API and insert the following code: + +```typescript +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from './api'; + +let _extApi: PythonEnvironmentApi | undefined; +export async function getEnvExtApi(): Promise { + if (_extApi) { + return _extApi; + } + const extension = vscode.extensions.getExtension('ms-python.vscode-python-envs'); + if (!extension) { + throw new Error('Python Environments extension not found.'); + } + if (extension?.isActive) { + _extApi = extension.exports as PythonEnvironmentApi; + return _extApi; + } + + await extension.activate(); + + _extApi = extension.exports as PythonEnvironmentApi; + return _extApi; +} +``` + +Now you are ready to use it to register your environment manager. + +### Registering the environment manager + +Add the following code to your extension's `extension.ts` file: + +```typescript +import { ExtensionContext } from 'vscode'; +import { getEnvExtApi } from './pythonEnvsApi'; +import { SampleEnvManager } from './sampleEnvManager'; + +export async function activate(context: ExtensionContext): Promise { + const api = await getEnvExtApi(); + + const envManager = new SampleEnvManager(api); + context.subscriptions.push(api.registerEnvironmentManager(envManager)); +} +``` + +See full implementations for built-in support here: https://github.com/microsoft/vscode-python-environments/blob/main/src/managers diff --git a/examples/sample1/.vscode/extensions.json b/examples/sample1/.vscode/extensions.json new file mode 100644 index 0000000..e6a4ef6 --- /dev/null +++ b/examples/sample1/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "dbaeumer.vscode-eslint", + "amodio.tsl-problem-matcher", + "ms-vscode.extension-test-runner", + "ms-python.vscode-python-envs", + "esbenp.prettier-vscode" + ] +} diff --git a/examples/sample1/.vscode/launch.json b/examples/sample1/.vscode/launch.json new file mode 100644 index 0000000..befd8c5 --- /dev/null +++ b/examples/sample1/.vscode/launch.json @@ -0,0 +1,17 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/dist/**/*.js"], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/examples/sample1/.vscode/settings.json b/examples/sample1/.vscode/settings.json new file mode 100644 index 0000000..bf5eb7c --- /dev/null +++ b/examples/sample1/.vscode/settings.json @@ -0,0 +1,22 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false, // set this to true to hide the "out" folder with the compiled JS files + "dist": false // set this to true to hide the "dist" folder with the compiled JS files + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "dist": true // set this to false to include "dist" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "diffEditor.ignoreTrimWhitespace": false + }, + "prettier.tabWidth": 4 +} diff --git a/examples/sample1/.vscode/tasks.json b/examples/sample1/.vscode/tasks.json new file mode 100644 index 0000000..400c607 --- /dev/null +++ b/examples/sample1/.vscode/tasks.json @@ -0,0 +1,37 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$ts-webpack-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "type": "npm", + "script": "watch-tests", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never", + "group": "watchers" + }, + "group": "build" + }, + { + "label": "tasks: watch-tests", + "dependsOn": ["npm: watch", "npm: watch-tests"], + "problemMatcher": [] + } + ] +} diff --git a/examples/sample1/.vscodeignore b/examples/sample1/.vscodeignore new file mode 100644 index 0000000..d255964 --- /dev/null +++ b/examples/sample1/.vscodeignore @@ -0,0 +1,14 @@ +.vscode/** +.vscode-test/** +out/** +node_modules/** +src/** +.gitignore +.yarnrc +webpack.config.js +vsc-extension-quickstart.md +**/tsconfig.json +**/eslint.config.mjs +**/*.map +**/*.ts +**/.vscode-test.* diff --git a/examples/sample1/CHANGELOG.md b/examples/sample1/CHANGELOG.md new file mode 100644 index 0000000..eafe827 --- /dev/null +++ b/examples/sample1/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change Log + +All notable changes to the "sample1" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] + +- Initial release \ No newline at end of file diff --git a/examples/sample1/README.md b/examples/sample1/README.md new file mode 100644 index 0000000..ad00b5e --- /dev/null +++ b/examples/sample1/README.md @@ -0,0 +1,3 @@ +### Template example for Environment Manager + +This is a template for an environment manager extension. It demonstrates how to use the Python Environments API to register your custom environment manager. You can look at implementations for `venv`, and `conda` (here https://github.com/microsoft/vscode-python-environments/blob/main/src/managers) for more examples. diff --git a/examples/sample1/eslint.config.mjs b/examples/sample1/eslint.config.mjs new file mode 100644 index 0000000..baca243 --- /dev/null +++ b/examples/sample1/eslint.config.mjs @@ -0,0 +1,27 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; + +export default [{ + files: ["**/*.ts"], +}, { + plugins: { + "@typescript-eslint": typescriptEslint, + }, + + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: "module", + }, + + rules: { + "@typescript-eslint/naming-convention": ["warn", { + selector: "import", + format: ["camelCase", "PascalCase"], + }], + curly: "warn", + eqeqeq: "warn", + "no-throw-literal": "warn", + semi: "warn", + }, +}]; \ No newline at end of file diff --git a/examples/sample1/package-lock.json b/examples/sample1/package-lock.json new file mode 100644 index 0000000..269e9f0 --- /dev/null +++ b/examples/sample1/package-lock.json @@ -0,0 +1,4659 @@ +{ + "name": "sample1", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample1", + "version": "0.0.1", + "devDependencies": { + "@types/mocha": "^10.0.7", + "@types/node": "20.x", + "@types/vscode": "^1.95.0", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1", + "eslint": "^9.9.1", + "ts-loader": "^9.5.1", + "typescript": "^5.5.4", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4" + }, + "engines": { + "vscode": "^1.95.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.5", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.9.1.tgz", + "integrity": "sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", + "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.9.tgz", + "integrity": "sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/vscode": { + "version": "1.95.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", + "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", + "integrity": "sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/type-utils": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", + "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz", + "integrity": "sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz", + "integrity": "sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.17.0", + "@typescript-eslint/utils": "8.17.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.17.0.tgz", + "integrity": "sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz", + "integrity": "sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/visitor-keys": "8.17.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.17.0.tgz", + "integrity": "sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.17.0", + "@typescript-eslint/types": "8.17.0", + "@typescript-eslint/typescript-estree": "8.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz", + "integrity": "sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.17.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.10.tgz", + "integrity": "sha512-B0mMH4ia+MOOtwNiLi79XhA+MLmUItIC8FckEuKrVAVriIuSWjt7vv4+bF8qVFiNFe4QRfzPaIZk39FZGWEwHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mocha": "^10.0.2", + "c8": "^9.1.0", + "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" + }, + "bin": { + "vscode-test": "out/bin.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.1.tgz", + "integrity": "sha512-Gc6EdaLANdktQ1t+zozoBVRynfIsMKMc94Svu1QreOBC8y76x4tvaK32TljrLi1LI2+PK58sDVbL7ALdqf3VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^7.0.1", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.24.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", + "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001669", + "electron-to-chromium": "^1.5.41", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/c8": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001687", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz", + "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.71", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz", + "integrity": "sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.9.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.16.0", + "@eslint/plugin-kit": "^0.2.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.5", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "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, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "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, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/mocha/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "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, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/examples/sample1/package.json b/examples/sample1/package.json new file mode 100644 index 0000000..060fd35 --- /dev/null +++ b/examples/sample1/package.json @@ -0,0 +1,40 @@ +{ + "name": "sample1", + "displayName": "Sample1", + "description": "A sample environment manager", + "version": "0.0.1", + "engines": { + "vscode": "^1.95.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [], + "main": "./dist/extension.js", + "contributes": {}, + "scripts": { + "vscode:prepublish": "npm run package", + "compile": "webpack", + "watch": "webpack --watch", + "package": "webpack --mode production --devtool hidden-source-map", + "compile-tests": "tsc -p . --outDir out", + "watch-tests": "tsc -p . -w --outDir out", + "pretest": "npm run compile-tests && npm run compile && npm run lint", + "lint": "eslint src", + "test": "vscode-test" + }, + "devDependencies": { + "@types/vscode": "^1.95.0", + "@types/mocha": "^10.0.7", + "@types/node": "20.x", + "@typescript-eslint/eslint-plugin": "^8.3.0", + "@typescript-eslint/parser": "^8.3.0", + "eslint": "^9.9.1", + "typescript": "^5.5.4", + "ts-loader": "^9.5.1", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4", + "@vscode/test-cli": "^0.0.10", + "@vscode/test-electron": "^2.4.1" + } +} diff --git a/examples/sample1/src/api.ts b/examples/sample1/src/api.ts new file mode 100644 index 0000000..689a85a --- /dev/null +++ b/examples/sample1/src/api.ts @@ -0,0 +1,1254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; + +/** + * The path to an icon, or a theme-specific configuration of icons. + */ +export type IconPath = + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + +/** + * Options for executing a Python executable. + */ +export interface PythonCommandRunConfiguration { + /** + * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path + * to an executable that can be spawned. + */ + executable: string; + + /** + * Arguments to pass to the python executable. These arguments will be passed on all execute calls. + * This is intended for cases where you might want to do interpreter specific flags. + */ + args?: string[]; +} + +/** + * Contains details on how to use a particular python environment + * + * Running In Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + * Creating a Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + */ +export interface PythonEnvironmentExecutionInfo { + /** + * Details on how to run the python executable. + */ + run: PythonCommandRunConfiguration; + + /** + * Details on how to run the python executable after activating the environment. + * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. + */ + activatedRun?: PythonCommandRunConfiguration; + + /** + * Details on how to activate an environment. + */ + activation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to activate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.activation} if set. + */ + shellActivation?: Map; + + /** + * Details on how to deactivate an environment. + */ + deactivation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to deactivate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.deactivation} if set. + */ + shellDeactivation?: Map; +} + +/** + * Interface representing the ID of a Python environment. + */ +export interface PythonEnvironmentId { + /** + * The unique identifier of the Python environment. + */ + id: string; + + /** + * The ID of the manager responsible for the Python environment. + */ + managerId: string; +} + +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Interface representing information about a Python environment. + */ +export interface PythonEnvironmentInfo { + /** + * The name of the Python environment. + */ + readonly name: string; + + /** + * The display name of the Python environment. + */ + readonly displayName: string; + + /** + * The short display name of the Python environment. + */ + readonly shortDisplayName?: string; + + /** + * The display path of the Python environment. + */ + readonly displayPath: string; + + /** + * The version of the Python environment. + */ + readonly version: string; + + /** + * Path to the python binary or environment folder. + */ + readonly environmentPath: Uri; + + /** + * The description of the Python environment. + */ + readonly description?: string; + + /** + * The tooltip for the Python environment, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Information on how to execute the Python environment. This is required for executing Python code in the environment. + */ + readonly execInfo: PythonEnvironmentExecutionInfo; + + /** + * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. + * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. + */ + readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; +} + +/** + * Interface representing a Python environment. + */ +export interface PythonEnvironment extends PythonEnvironmentInfo { + /** + * The ID of the Python environment. + */ + readonly envId: PythonEnvironmentId; +} + +/** + * Type representing the scope for setting a Python environment. + * Can be undefined or a URI. + */ +export type SetEnvironmentScope = undefined | Uri | Uri[]; + +/** + * Type representing the scope for getting a Python environment. + * Can be undefined or a URI. + */ +export type GetEnvironmentScope = undefined | Uri; + +/** + * Type representing the scope for creating a Python environment. + * Can be a Python project or 'global'. + */ +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; +/** + * The scope for which environments are to be refreshed. + * - `undefined`: Search for environments globally and workspaces. + * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. + */ +export type RefreshEnvironmentsScope = Uri | undefined; + +/** + * The scope for which environments are required. + * - `"all"`: All environments. + * - `"global"`: Python installations that are usually a base for creating virtual environments. + * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. + */ +export type GetEnvironmentsScope = Uri | 'all' | 'global'; + +/** + * Event arguments for when the current Python environment changes. + */ +export type DidChangeEnvironmentEventArgs = { + /** + * The URI of the environment that changed. + */ + readonly uri: Uri | undefined; + + /** + * The old Python environment before the change. + */ + readonly old: PythonEnvironment | undefined; + + /** + * The new Python environment after the change. + */ + readonly new: PythonEnvironment | undefined; +}; + +/** + * Enum representing the kinds of environment changes. + */ +export enum EnvironmentChangeKind { + /** + * Indicates that an environment was added. + */ + add = 'add', + + /** + * Indicates that an environment was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when the list of Python environments changes. + */ +export type DidChangeEnvironmentsEventArgs = { + /** + * The kind of change that occurred (add or remove). + */ + kind: EnvironmentChangeKind; + + /** + * The Python environment that was added or removed. + */ + environment: PythonEnvironment; +}[]; + +/** + * Type representing the context for resolving a Python environment. + */ +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} + +/** + * Interface representing an environment manager. + */ +export interface EnvironmentManager { + /** + * The name of the environment manager. + */ + readonly name: string; + + /** + * The display name of the environment manager. + */ + readonly displayName?: string; + + /** + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' + */ + readonly preferredPackageManagerId: string; + + /** + * The description of the environment manager. + */ + readonly description?: string; + + /** + * The tooltip for the environment manager, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The log output channel for the environment manager. + */ + readonly log?: LogOutputChannel; + + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. + * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. + * @returns A promise that resolves to the created Python environment, or undefined if creation failed. + */ + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; + + /** + * Removes the specified Python environment. + * @param environment - The Python environment to remove. + * @returns A promise that resolves when the environment is removed. + */ + remove?(environment: PythonEnvironment): Promise; + + /** + * Refreshes the list of Python environments within the specified scope. + * @param scope - The scope within which to refresh environments. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event; + + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + * @returns A promise that resolves when the environment is set. + */ + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + get(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event; + + /** + * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. + * + * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: + * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. + * - A {@link Uri} object, which typically represents either: + * - A folder that contains the Python environment. + * - The path to a Python executable. + * + * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. + * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. + */ + resolve(context: ResolveEnvironmentContext): Promise; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a package ID. + */ +export interface PackageId { + /** + * The ID of the package. + */ + id: string; + + /** + * The ID of the package manager. + */ + managerId: string; + + /** + * The ID of the environment in which the package is installed. + */ + environmentId: string; +} + +/** + * Interface representing package information. + */ +export interface PackageInfo { + /** + * The name of the package. + */ + readonly name: string; + + /** + * The display name of the package. + */ + readonly displayName: string; + + /** + * The version of the package. + */ + readonly version?: string; + + /** + * The description of the package. + */ + readonly description?: string; + + /** + * The tooltip for the package, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The URIs associated with the package. + */ + readonly uris?: readonly Uri[]; +} + +/** + * Interface representing a package. + */ +export interface Package extends PackageInfo { + /** + * The ID of the package. + */ + readonly pkgId: PackageId; +} + +/** + * Enum representing the kinds of package changes. + */ +export enum PackageChangeKind { + /** + * Indicates that a package was added. + */ + add = 'add', + + /** + * Indicates that a package was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when packages change. + */ +export interface DidChangePackagesEventArgs { + /** + * The Python environment in which the packages changed. + */ + environment: PythonEnvironment; + + /** + * The package manager responsible for the changes. + */ + manager: PackageManager; + + /** + * The list of changes, each containing the kind of change and the package affected. + */ + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +/** + * Interface representing a package manager. + */ +export interface PackageManager { + /** + * The name of the package manager. + */ + name: string; + + /** + * The display name of the package manager. + */ + displayName?: string; + + /** + * The description of the package manager. + */ + description?: string; + + /** + * The tooltip for the package manager, which can be a string or a Markdown string. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + iconPath?: IconPath; + + /** + * The log output channel for the package manager. + */ + log?: LogOutputChannel; + + /** + * Installs/Uninstall packages in the specified Python environment. + * @param environment - The Python environment in which to install packages. + * @param packages - The packages to install. + * @returns A promise that resolves when the installation is complete. + */ + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; + + /** + * Refreshes the package list for the specified Python environment. + * @param environment - The Python environment for which to refresh the package list. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of packages for the specified Python environment. + * @param environment - The Python environment for which to retrieve packages. + * @returns An array of packages, or undefined if the packages could not be retrieved. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a Python project. + */ +export interface PythonProject { + /** + * The name of the Python project. + */ + readonly name: string; + + /** + * The URI of the Python project. + */ + readonly uri: Uri; + + /** + * The description of the Python project. + */ + readonly description?: string; + + /** + * The tooltip for the Python project, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; +} + +/** + * Options for creating a Python project. + */ +export interface PythonProjectCreatorOptions { + /** + * The name of the Python project. + */ + name: string; + + /** + * Optional path that may be provided as a root for the project. + */ + uri?: Uri; +} + +/** + * Interface representing a creator for Python projects. + */ +export interface PythonProjectCreator { + /** + * The name of the Python project creator. + */ + readonly name: string; + + /** + * The display name of the Python project creator. + */ + readonly displayName?: string; + + /** + * The description of the Python project creator. + */ + readonly description?: string; + + /** + * The tooltip for the Python project creator, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * Creates a new Python project or projects. + * @param options - Optional parameters for creating the Python project. + * @returns A promise that resolves to a Python project, an array of Python projects, or undefined. + */ + create(options?: PythonProjectCreatorOptions): Promise; +} + +/** + * Event arguments for when Python projects change. + */ +export interface DidChangePythonProjectsEventArgs { + /** + * The list of Python projects that were added. + */ + added: PythonProject[]; + + /** + * The list of Python projects that were removed. + */ + removed: PythonProject[]; +} + +/** + * Options for package management. + */ +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if it is already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if it is already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} + +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { + /** + * The process ID of the Python process. + */ + readonly pid?: number; + + /** + * The standard input of the Python process. + */ + readonly stdin: NodeJS.WritableStream; + + /** + * The standard output of the Python process. + */ + readonly stdout: NodeJS.ReadableStream; + + /** + * The standard error of the Python process. + */ + readonly stderr: NodeJS.ReadableStream; + + /** + * Kills the Python process. + */ + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; +} + +export interface PythonEnvironmentManagerRegistrationApi { + /** + * Register an environment manager implementation. + * + * @param manager Environment Manager implementation to register. + * @returns A disposable that can be used to unregister the environment manager. + * @see {@link EnvironmentManager} + */ + registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} + +export interface PythonEnvironmentItemApi { + /** + * Create a Python environment item from the provided environment info. This item is used to interact + * with the environment. + * + * @param info Some details about the environment like name, version, etc. needed to interact with the environment. + * @param manager The environment manager to associate with the environment. + * @returns The Python environment. + */ + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} + +export interface PythonEnvironmentManagementApi { + /** + * Create a Python environment using environment manager associated with the scope. + * + * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. + * @returns The Python environment created. `undefined` if not created. + */ + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; + + /** + * Remove a Python environment. + * + * @param environment The Python environment to remove. + * @returns A promise that resolves when the environment has been removed. + */ + removeEnvironment(environment: PythonEnvironment): Promise; +} + +export interface PythonEnvironmentsApi { + /** + * Initiates a refresh of Python environments within the specified scope. + * @param scope - The scope within which to search for environments. + * @returns A promise that resolves when the search is complete. + */ + refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event; + + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + */ + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + getEnvironment(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event; +} + +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { + /** + * Register a package manager implementation. + * + * @param manager Package Manager implementation to register. + * @returns A disposable that can be used to unregister the package manager. + * @see {@link PackageManager} + */ + registerPackageManager(manager: PackageManager): Disposable; +} + +export interface PythonPackageGetterApi { + /** + * Refresh the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is to be refreshed. + * @returns A promise that resolves when the list of packages has been refreshed. + */ + refreshPackages(environment: PythonEnvironment): Promise; + + /** + * Get the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is required. + * @returns The list of packages in the Python Environment. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event; +} + +export interface PythonPackageItemApi { + /** + * Create a package item from the provided package info. + * + * @param info The package info. + * @param environment The Python Environment in which the package is installed. + * @param manager The package manager that installed the package. + * @returns The package item. + */ + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; +} + +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + +/** + * The API for interacting with Python projects. A project in python is any folder or file that is a contained + * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, + * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. + * + * By default all `vscode.workspace.workspaceFolders` are treated as projects. + */ +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { + /** + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. + * + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. + */ + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { + /** + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. + * + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. + */ + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; + + /** + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. + */ + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} + +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { + /** + * Name of the task to run. + */ + name: string; + + /** + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. + * + */ + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; + + /** + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeTye: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. + * + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required. If not provided, + * it fetches the environment variables for the global scope. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. + */ + getEnvironmentVariables( + uri: Uri | undefined, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; +} + +/** + * The API for interacting with Python environments, package managers, and projects. + */ +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} diff --git a/examples/sample1/src/extension.ts b/examples/sample1/src/extension.ts new file mode 100644 index 0000000..69540dd --- /dev/null +++ b/examples/sample1/src/extension.ts @@ -0,0 +1,20 @@ +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; +import { getEnvExtApi } from './pythonEnvsApi'; +import { SampleEnvManager } from './sampleEnvManager'; + +// This method is called when your extension is activated +// Your extension is activated the very first time the command is executed +export async function activate(context: vscode.ExtensionContext) { + const api = await getEnvExtApi(); + + const log = vscode.window.createOutputChannel('Sample Environment Manager', { log: true }); + context.subscriptions.push(log); + + const manager = new SampleEnvManager(log); + context.subscriptions.push(api.registerEnvironmentManager(manager)); +} + +// This method is called when your extension is deactivated +export function deactivate() {} diff --git a/examples/sample1/src/pythonEnvsApi.ts b/examples/sample1/src/pythonEnvsApi.ts new file mode 100644 index 0000000..888262a --- /dev/null +++ b/examples/sample1/src/pythonEnvsApi.ts @@ -0,0 +1,22 @@ +import * as vscode from 'vscode'; +import { PythonEnvironmentApi } from './api'; + +let _extApi: PythonEnvironmentApi | undefined; +export async function getEnvExtApi(): Promise { + if (_extApi) { + return _extApi; + } + const extension = vscode.extensions.getExtension('ms-python.vscode-python-envs'); + if (!extension) { + throw new Error('Python Environments extension not found.'); + } + if (extension?.isActive) { + _extApi = extension.exports as PythonEnvironmentApi; + return _extApi; + } + + await extension.activate(); + + _extApi = extension.exports as PythonEnvironmentApi; + return _extApi; +} diff --git a/examples/sample1/src/sampleEnvManager.ts b/examples/sample1/src/sampleEnvManager.ts new file mode 100644 index 0000000..ee13d88 --- /dev/null +++ b/examples/sample1/src/sampleEnvManager.ts @@ -0,0 +1,95 @@ +import { MarkdownString, LogOutputChannel, Event } from 'vscode'; +import { + CreateEnvironmentOptions, + CreateEnvironmentScope, + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + QuickCreateConfig, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from './api'; + +export class SampleEnvManager implements EnvironmentManager { + name: string; + displayName?: string | undefined; + preferredPackageManagerId: string; + description?: string | undefined; + tooltip?: string | MarkdownString | undefined; + iconPath?: IconPath | undefined; + log?: LogOutputChannel | undefined; + + constructor(log: LogOutputChannel) { + this.name = 'sample'; + this.displayName = 'Sample'; + this.preferredPackageManagerId = 'my-publisher.sample:sample'; + // if you want to use builtin `pip` then use this + // this.preferredPackageManagerId = 'ms-python.python:pip'; + this.log = log; + } + + quickCreateConfig(): QuickCreateConfig | undefined { + // Code to provide quick create configuration goes here + + throw new Error('Method not implemented.'); + } + + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise { + // Code to handle creating environments goes here + + throw new Error('Method not implemented.'); + } + remove?(environment: PythonEnvironment): Promise { + // Code to handle removing environments goes here + + throw new Error('Method not implemented.'); + } + refresh(scope: RefreshEnvironmentsScope): Promise { + // Code to handle refreshing environments goes here + // This is called when the user clicks on the refresh button in the UI + + throw new Error('Method not implemented.'); + } + getEnvironments(scope: GetEnvironmentsScope): Promise { + // Code to get the list of environments goes here + // This may be called when the python extension is activated to get the list of environments + + throw new Error('Method not implemented.'); + } + + // Event to be raised with the list of available extensions changes for this manager + onDidChangeEnvironments?: Event | undefined; + + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + // User selected a environment for the given scope + // undefined environment means user wants to reset the environment for the given scope + + throw new Error('Method not implemented.'); + } + get(scope: GetEnvironmentScope): Promise { + // Code to get the environment for the given scope goes here + + throw new Error('Method not implemented.'); + } + + // Event to be raised when the environment for any active scope changes + onDidChangeEnvironment?: Event | undefined; + + resolve(context: ResolveEnvironmentContext): Promise { + // Code to resolve the environment goes here. Resolving an environment means + // to convert paths to actual environments + + throw new Error('Method not implemented.'); + } + + clearCache?(): Promise { + // Code to clear any cached data goes here + + throw new Error('Method not implemented.'); + } +} diff --git a/examples/sample1/tsconfig.json b/examples/sample1/tsconfig.json new file mode 100644 index 0000000..e11381a --- /dev/null +++ b/examples/sample1/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2020", + "lib": ["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 + } +} diff --git a/examples/sample1/webpack.config.js b/examples/sample1/webpack.config.js new file mode 100644 index 0000000..37d7024 --- /dev/null +++ b/examples/sample1/webpack.config.js @@ -0,0 +1,48 @@ +//@ts-check + +'use strict'; + +const path = require('path'); + +//@ts-check +/** @typedef {import('webpack').Configuration} WebpackConfig **/ + +/** @type WebpackConfig */ +const extensionConfig = { + target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ + mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + + entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ + output: { + // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ + path: path.resolve(__dirname, 'dist'), + filename: 'extension.js', + libraryTarget: 'commonjs2' + }, + externals: { + vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ + // modules added here also need to be added in the .vscodeignore file + }, + resolve: { + // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader + extensions: ['.ts', '.js'] + }, + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + use: [ + { + loader: 'ts-loader' + } + ] + } + ] + }, + devtool: 'nosources-source-map', + infrastructureLogging: { + level: "log", // enables logging required for problem matchers + }, +}; +module.exports = [ extensionConfig ]; \ No newline at end of file diff --git a/files/common_packages.txt b/files/common_packages.txt deleted file mode 100644 index 46de877..0000000 --- a/files/common_packages.txt +++ /dev/null @@ -1,883 +0,0 @@ -accelerate -accounts -actions -addict -agent -agents -ai -aioboto3 -aiobotocore -aiofiles -aiogram -aiohttp -aiokafka -aiomysql -aioredis -aiosqlite -akshare -albumentations -alembic -algorithms -altair -analysis -analytics -anndata -ansible -anthropic -anyio -apex -api -apiclient -appdirs -AppKit -application -arcgis -args -arguments -arrow -art -ase -asgiref -astropy -asyncpg -attr -attrs -audio -auditlog -auth -authentication -autogen -av -aws -awswrangler -azure -azureml -babel -backend -backoff -backports -backtrader -barcode -base58 -basicsr -bcrypt -beanie -behave -billiard -binance -bitsandbytes -black -bleach -bleak -blinker -blog -board -bokeh -boto -boto3 -botocore -bottle -bpy -branca -bs4 -bson -c -cache -cachetools -caffe2 -callbacks -camera -cantools -captcha -carla -catboost -ccxt -celery -certifi -cffi -cftime -chainlit -channels -chardet -chat -chatbot -chess -chromadb -classes -clearml -click -clients -clip -cloudinary -cloudpickle -cloudscraper -cohere -colorama -coloredlogs -colorlog -common -commons -company -comtypes -conan -conf -config -configs -configuration -connection -connections -connectors -connexion -constant -constants -constructs -control -controller -coremltools -coverage -crcmod -credentials -crewai -croniter -cryptography -cs50 -cupy -customtkinter -cv -cvxpy -cvzone -cycler -Cython -dacite -dags -dagster -dao -dash -dashboard -dashscope -dask -data -databases -databricks -datadog -dataloader -dataset -datasets -dateparser -db -ddddocr -ddtrace -deap -debugpy -decorator -decorators -decord -decouple -deepdiff -deepface -deepspeed -defusedxml -delta -deltalake -dependencies -deprecation -dgl -diffusers -dill -discord -diskcache -distributed -distro -dlib -docker -docopt -docutils -docx -docx2pdf -docx2txt -docxtpl -domain -dotenv -DrissionPage -dropbox -duckdb -dynaconf -easydict -easygui -easyocr -ecdsa -ee -einops -elasticsearch -elevenlabs -emails -emoji -enums -env -environment -environs -envs -eval -evaluate -evaluation -event -eventlet -examples -exceptiongroup -export -extensions -ezdxf -fabric -fairseq -fakeredis -falcon -fastapi -fastcluster -fasttext -features -feedparser -ffmpeg -filelock -files -filetype -filterpy -filters -fiona -fire -fitz -FlagEmbedding -flasgger -flax -flet -folium -fpdf -freezegun -frontend -fsspec -ftfy -function -functions -future -fuzzywuzzy -fvcore -game -gcsfs -gdown -genpy -gensim -geocoder -geojson -geopandas -geopy -gevent -github -gitlab -globals -gnupg -google -googlemaps -googletrans -gpiozero -GPUtil -gql -gradio -graph -graphene -graphlib -graphql -graphviz -greenlet -groq -grpc -gspread -guardian -gunicorn -gurobipy -gym -gymnasium -h5py -handler -haversine -haystack -hdbscan -helper -helper_functions -helpers -holidays -home -html2text -html5lib -httpcore -httplib2 -httpx -humanize -hvac -hvplot -hyperopt -hypothesis -icecream -idna -ijson -image -imageio -img2pdf -imutils -inference -inflect -inflection -influxdb -infrastructure -injector -inquirer -insightface -instaloader -instructor -InstructorEmbedding -interfaces -inventory -invoke -ipdb -ipykernel -ipywidgets -isodate -itemadapter -itsdangerous -jax -jaxtyping -jieba -jira -jmespath -joblib -jose -jsii -json5 -jsonfield -jsonlines -jsonpickle -jsonschema -jwt -kafka -keras -keyboard -keycloak -keyring -kfp -kivymd -kombu -kornia -kubernetes -langchain -langdetect -langfuse -langgraph -langserve -langsmith -launch -layers -layout -ldap -ldap3 -ldm -Levenshtein -lib -librosa -libs -lightgbm -lightning -litellm -llm -lmdb -lmfit -loader -locust -logger -logic -login -loguru -loss -lpips -lxml -ma -mangum -mariadb -markdown2 -markdownify -marshmallow -mat -mathutils -matplotlib -maya -mediapipe -messages -MetaTrader5 -metrics -middleware -minio -mistralai -mkl -mlflow -mlxtend -mmcv -mmdet -mmdet3d -mmengine -mmseg -mne -mock -models -modelscope -monai -mongoengine -monitoring -moto -motor -mouse -moviepy -mpi4py -mplcursors -mplfinance -mpmath -msal -msgpack -msgspec -msrest -mss -mujoco -multidict -multipart -multiprocess -multitasking -munch -mupdf -mutagen -mysql -natsort -nbformat -neo4j -netaddr -netCDF4 -netifaces -netmiko -nets -networks -networkx -newspaper -nibabel -nicegui -ninja -nltk -notification -notifications -nu -num2words -numba -numpy -numpypy -o -oauth2client -oauthlib -oci -office365 -ollama -omegaconf -onnx -onnxruntime -onnxsim -open3d -openai -opencensus -openpyxl -openvino -operators -optax -optimum -options -optuna -oracledb -order -orjson -ortools -osmnx -oss2 -overrides -p -pa -packages -packaging -paddle -paddleocr -pafy -pages -panda -pandas -pandasql -pandera -panel -parameterized -parameters -paramiko -params -parse -parsel -passlib -path -pathlib2 -pathspec -payment -payments -pdf2docx -pdf2image -pdfkit -pdfminer -pdfplumber -peewee -peft -pendulum -pexpect -pg8000 -pgvector -phonenumbers -picamera2 -piexif -pika -pikepdf -PIL -pinecone -ping3 -pip -pipeline -pipelines -platformdirs -player -playsound -playwright -plot -plotly -pluggy -plyer -plyfile -pmdarima -pocketsphinx -polars -praw -prefect -preprocess -preprocessing -prettytable -processing -processor -product -products -progress -progressbar -projects -prompt -prophet -psutil -psycopg -psycopg2 -publication -pulumi -py -py4j -py7zr -pyarrow -pybullet -pycocotools -pycountry -pycurl -pydantic -pydash -pydicom -pydub -pyecharts -pyfiglet -pygame -pyglet -pygsheets -pyjokes -pymavlink -pymilvus -pymodbus -pymongo -pymssql -pymsteams -pynamodb -pynput -pynvml -pyodbc -pyogrio -pyotp -pypandoc -pyparsing -pypdf -PyPDF2 -pyperclip -pypinyin -pyppeteer -pyproj -PyQt5 -PyQt6 -pyqtgraph -pyquaternion -pyrealsense2 -pysam -pysftp -pyshark -PySide2 -PySide6 -PySimpleGUI -pyspark -pystray -pystyle -pytesseract -pytest -pytorch3d -pyttsx3 -pytube -pytubefix -pytz -pyvis -pyvista -pywhatkit -pywinauto -pyzbar -qiskit -qrcode -queries -query -rag -ragas -rasterio -ratelimit -ray -razorpay -rdflib -rdkit -re2 -redis -regex -rembg -replicate -reportlab -reports -requests -responses -retry -retrying -reversion -rich -rioxarray -rospkg -routers -rq -rsa -run -s3fs -safetensors -sagemaker -sam2 -sanic -scanpy -scapy -schedule -scheduler -schema -schemas -scipy -scp -screeninfo -script -scripts -seaborn -secret -security -selenium -seleniumbase -semver -sendgrid -sentencepiece -serial -serpapi -service -services -setproctitle -sets -setuptools -shap -shapely -shared -shiboken6 -shortuuid -SimpleITK -simplejson -simulation -sip -six -sklearn -slack -slowapi -smbus -sniffio -snowflake -socketio -solver -sortedcontainers -sounddevice -soundfile -source -spacy -spotipy -sql -sqlglot -sqlmodel -sqlparse -src -srt -sshtunnel -starlette -state -stats -statsmodels -storage -store -streamlit -stripe -structlog -suds -supabase -supervision -support -sympy -ta -tables -tablib -tabula -tabulate -task -tasks -telebot -telegram -templates -temporalio -tenacity -tensorboard -tensorboardX -tensorflow -tensorflowjs -tensorrt -termcolor -textblob -thefuzz -thop -thread -threadpoolctl -tifffile -tiktoken -timezonefinder -timm -tinydb -tkcalendar -tkinterdnd2 -tldextract -tokenizers -toml -tomli -tool -tools -toolz -torch -torchaudio -torchinfo -torchmetrics -torchsummary -torchvision -tornado -tortoise -tqdm -tracker -train -trainer -traitlets -transform -transformers -transforms -transforms3d -translate -trimesh -trino -trio -triton -trl -ttkbootstrap -ttkthemes -TTS -tushare -tweepy -twilio -typeguard -typer -tyro -tzlocal -ujson -ulid -ultralytics -umap -unsloth -unstructured -uritemplate -urllib3 -utils -utime -utm -uvicorn -uvloop -validation -validators -version -vertexai -vine -visualization -vllm -vosk -vtk -w3lib -wagtail -waitress -wandb -watchdog -weasyprint -web -web3 -webdataset -website -websocket -websockets -webview -wget -whisper -whois -wikipedia -win32gui -wordcloud -worker -workflow -workflows -wrapt -xarray -xformers -xgboost -xhtml2pdf -xlrd -xlwings -xlwt -xmltodict -xxhash -yacs -yarl -yfinance -youtube_dl -zarr -zeep -zhipuai -zstandard \ No newline at end of file diff --git a/files/common_pip_packages.json b/files/common_pip_packages.json new file mode 100644 index 0000000..7561c63 --- /dev/null +++ b/files/common_pip_packages.json @@ -0,0 +1,12054 @@ +[ + { + "name": "about-time", + "uri": "https://pypi.org/project/about-time" + }, + { + "name": "absl-py", + "uri": "https://pypi.org/project/absl-py" + }, + { + "name": "accelerate", + "uri": "https://pypi.org/project/accelerate" + }, + { + "name": "accesscontrol", + "uri": "https://pypi.org/project/accesscontrol" + }, + { + "name": "accessible-pygments", + "uri": "https://pypi.org/project/accessible-pygments" + }, + { + "name": "acme", + "uri": "https://pypi.org/project/acme" + }, + { + "name": "acquisition", + "uri": "https://pypi.org/project/acquisition" + }, + { + "name": "acryl-datahub", + "uri": "https://pypi.org/project/acryl-datahub" + }, + { + "name": "acryl-datahub-airflow-plugin", + "uri": "https://pypi.org/project/acryl-datahub-airflow-plugin" + }, + { + "name": "adagio", + "uri": "https://pypi.org/project/adagio" + }, + { + "name": "adal", + "uri": "https://pypi.org/project/adal" + }, + { + "name": "addict", + "uri": "https://pypi.org/project/addict" + }, + { + "name": "adlfs", + "uri": "https://pypi.org/project/adlfs" + }, + { + "name": "aenum", + "uri": "https://pypi.org/project/aenum" + }, + { + "name": "affine", + "uri": "https://pypi.org/project/affine" + }, + { + "name": "agate", + "uri": "https://pypi.org/project/agate" + }, + { + "name": "aio-pika", + "uri": "https://pypi.org/project/aio-pika" + }, + { + "name": "aioboto3", + "uri": "https://pypi.org/project/aioboto3" + }, + { + "name": "aiobotocore", + "uri": "https://pypi.org/project/aiobotocore" + }, + { + "name": "aiodns", + "uri": "https://pypi.org/project/aiodns" + }, + { + "name": "aiofiles", + "uri": "https://pypi.org/project/aiofiles" + }, + { + "name": "aiogram", + "uri": "https://pypi.org/project/aiogram" + }, + { + "name": "aiohappyeyeballs", + "uri": "https://pypi.org/project/aiohappyeyeballs" + }, + { + "name": "aiohttp", + "uri": "https://pypi.org/project/aiohttp" + }, + { + "name": "aiohttp-cors", + "uri": "https://pypi.org/project/aiohttp-cors" + }, + { + "name": "aiohttp-retry", + "uri": "https://pypi.org/project/aiohttp-retry" + }, + { + "name": "aioitertools", + "uri": "https://pypi.org/project/aioitertools" + }, + { + "name": "aiokafka", + "uri": "https://pypi.org/project/aiokafka" + }, + { + "name": "aiomqtt", + "uri": "https://pypi.org/project/aiomqtt" + }, + { + "name": "aiomultiprocess", + "uri": "https://pypi.org/project/aiomultiprocess" + }, + { + "name": "aioredis", + "uri": "https://pypi.org/project/aioredis" + }, + { + "name": "aioresponses", + "uri": "https://pypi.org/project/aioresponses" + }, + { + "name": "aiormq", + "uri": "https://pypi.org/project/aiormq" + }, + { + "name": "aiosignal", + "uri": "https://pypi.org/project/aiosignal" + }, + { + "name": "aiosqlite", + "uri": "https://pypi.org/project/aiosqlite" + }, + { + "name": "ajsonrpc", + "uri": "https://pypi.org/project/ajsonrpc" + }, + { + "name": "alabaster", + "uri": "https://pypi.org/project/alabaster" + }, + { + "name": "albucore", + "uri": "https://pypi.org/project/albucore" + }, + { + "name": "albumentations", + "uri": "https://pypi.org/project/albumentations" + }, + { + "name": "alembic", + "uri": "https://pypi.org/project/alembic" + }, + { + "name": "algoliasearch", + "uri": "https://pypi.org/project/algoliasearch" + }, + { + "name": "alive-progress", + "uri": "https://pypi.org/project/alive-progress" + }, + { + "name": "aliyun-python-sdk-core", + "uri": "https://pypi.org/project/aliyun-python-sdk-core" + }, + { + "name": "aliyun-python-sdk-kms", + "uri": "https://pypi.org/project/aliyun-python-sdk-kms" + }, + { + "name": "allure-pytest", + "uri": "https://pypi.org/project/allure-pytest" + }, + { + "name": "allure-python-commons", + "uri": "https://pypi.org/project/allure-python-commons" + }, + { + "name": "altair", + "uri": "https://pypi.org/project/altair" + }, + { + "name": "altgraph", + "uri": "https://pypi.org/project/altgraph" + }, + { + "name": "amazon-ion", + "uri": "https://pypi.org/project/amazon-ion" + }, + { + "name": "amqp", + "uri": "https://pypi.org/project/amqp" + }, + { + "name": "amqpstorm", + "uri": "https://pypi.org/project/amqpstorm" + }, + { + "name": "analytics-python", + "uri": "https://pypi.org/project/analytics-python" + }, + { + "name": "aniso8601", + "uri": "https://pypi.org/project/aniso8601" + }, + { + "name": "annotated-types", + "uri": "https://pypi.org/project/annotated-types" + }, + { + "name": "annoy", + "uri": "https://pypi.org/project/annoy" + }, + { + "name": "ansi2html", + "uri": "https://pypi.org/project/ansi2html" + }, + { + "name": "ansible", + "uri": "https://pypi.org/project/ansible" + }, + { + "name": "ansible-compat", + "uri": "https://pypi.org/project/ansible-compat" + }, + { + "name": "ansible-core", + "uri": "https://pypi.org/project/ansible-core" + }, + { + "name": "ansible-lint", + "uri": "https://pypi.org/project/ansible-lint" + }, + { + "name": "ansicolors", + "uri": "https://pypi.org/project/ansicolors" + }, + { + "name": "ansiwrap", + "uri": "https://pypi.org/project/ansiwrap" + }, + { + "name": "anthropic", + "uri": "https://pypi.org/project/anthropic" + }, + { + "name": "antlr4-python3-runtime", + "uri": "https://pypi.org/project/antlr4-python3-runtime" + }, + { + "name": "antlr4-tools", + "uri": "https://pypi.org/project/antlr4-tools" + }, + { + "name": "anyascii", + "uri": "https://pypi.org/project/anyascii" + }, + { + "name": "anybadge", + "uri": "https://pypi.org/project/anybadge" + }, + { + "name": "anyio", + "uri": "https://pypi.org/project/anyio" + }, + { + "name": "anytree", + "uri": "https://pypi.org/project/anytree" + }, + { + "name": "apache-airflow", + "uri": "https://pypi.org/project/apache-airflow" + }, + { + "name": "apache-airflow-providers-amazon", + "uri": "https://pypi.org/project/apache-airflow-providers-amazon" + }, + { + "name": "apache-airflow-providers-apache-spark", + "uri": "https://pypi.org/project/apache-airflow-providers-apache-spark" + }, + { + "name": "apache-airflow-providers-atlassian-jira", + "uri": "https://pypi.org/project/apache-airflow-providers-atlassian-jira" + }, + { + "name": "apache-airflow-providers-celery", + "uri": "https://pypi.org/project/apache-airflow-providers-celery" + }, + { + "name": "apache-airflow-providers-cncf-kubernetes", + "uri": "https://pypi.org/project/apache-airflow-providers-cncf-kubernetes" + }, + { + "name": "apache-airflow-providers-common-compat", + "uri": "https://pypi.org/project/apache-airflow-providers-common-compat" + }, + { + "name": "apache-airflow-providers-common-io", + "uri": "https://pypi.org/project/apache-airflow-providers-common-io" + }, + { + "name": "apache-airflow-providers-common-sql", + "uri": "https://pypi.org/project/apache-airflow-providers-common-sql" + }, + { + "name": "apache-airflow-providers-databricks", + "uri": "https://pypi.org/project/apache-airflow-providers-databricks" + }, + { + "name": "apache-airflow-providers-datadog", + "uri": "https://pypi.org/project/apache-airflow-providers-datadog" + }, + { + "name": "apache-airflow-providers-dbt-cloud", + "uri": "https://pypi.org/project/apache-airflow-providers-dbt-cloud" + }, + { + "name": "apache-airflow-providers-docker", + "uri": "https://pypi.org/project/apache-airflow-providers-docker" + }, + { + "name": "apache-airflow-providers-fab", + "uri": "https://pypi.org/project/apache-airflow-providers-fab" + }, + { + "name": "apache-airflow-providers-ftp", + "uri": "https://pypi.org/project/apache-airflow-providers-ftp" + }, + { + "name": "apache-airflow-providers-google", + "uri": "https://pypi.org/project/apache-airflow-providers-google" + }, + { + "name": "apache-airflow-providers-http", + "uri": "https://pypi.org/project/apache-airflow-providers-http" + }, + { + "name": "apache-airflow-providers-imap", + "uri": "https://pypi.org/project/apache-airflow-providers-imap" + }, + { + "name": "apache-airflow-providers-jdbc", + "uri": "https://pypi.org/project/apache-airflow-providers-jdbc" + }, + { + "name": "apache-airflow-providers-microsoft-azure", + "uri": "https://pypi.org/project/apache-airflow-providers-microsoft-azure" + }, + { + "name": "apache-airflow-providers-microsoft-mssql", + "uri": "https://pypi.org/project/apache-airflow-providers-microsoft-mssql" + }, + { + "name": "apache-airflow-providers-mongo", + "uri": "https://pypi.org/project/apache-airflow-providers-mongo" + }, + { + "name": "apache-airflow-providers-mysql", + "uri": "https://pypi.org/project/apache-airflow-providers-mysql" + }, + { + "name": "apache-airflow-providers-odbc", + "uri": "https://pypi.org/project/apache-airflow-providers-odbc" + }, + { + "name": "apache-airflow-providers-oracle", + "uri": "https://pypi.org/project/apache-airflow-providers-oracle" + }, + { + "name": "apache-airflow-providers-pagerduty", + "uri": "https://pypi.org/project/apache-airflow-providers-pagerduty" + }, + { + "name": "apache-airflow-providers-postgres", + "uri": "https://pypi.org/project/apache-airflow-providers-postgres" + }, + { + "name": "apache-airflow-providers-salesforce", + "uri": "https://pypi.org/project/apache-airflow-providers-salesforce" + }, + { + "name": "apache-airflow-providers-sftp", + "uri": "https://pypi.org/project/apache-airflow-providers-sftp" + }, + { + "name": "apache-airflow-providers-slack", + "uri": "https://pypi.org/project/apache-airflow-providers-slack" + }, + { + "name": "apache-airflow-providers-smtp", + "uri": "https://pypi.org/project/apache-airflow-providers-smtp" + }, + { + "name": "apache-airflow-providers-snowflake", + "uri": "https://pypi.org/project/apache-airflow-providers-snowflake" + }, + { + "name": "apache-airflow-providers-sqlite", + "uri": "https://pypi.org/project/apache-airflow-providers-sqlite" + }, + { + "name": "apache-airflow-providers-ssh", + "uri": "https://pypi.org/project/apache-airflow-providers-ssh" + }, + { + "name": "apache-airflow-providers-tableau", + "uri": "https://pypi.org/project/apache-airflow-providers-tableau" + }, + { + "name": "apache-beam", + "uri": "https://pypi.org/project/apache-beam" + }, + { + "name": "apache-sedona", + "uri": "https://pypi.org/project/apache-sedona" + }, + { + "name": "apispec", + "uri": "https://pypi.org/project/apispec" + }, + { + "name": "appdirs", + "uri": "https://pypi.org/project/appdirs" + }, + { + "name": "appium-python-client", + "uri": "https://pypi.org/project/appium-python-client" + }, + { + "name": "applicationinsights", + "uri": "https://pypi.org/project/applicationinsights" + }, + { + "name": "appnope", + "uri": "https://pypi.org/project/appnope" + }, + { + "name": "apprise", + "uri": "https://pypi.org/project/apprise" + }, + { + "name": "apscheduler", + "uri": "https://pypi.org/project/apscheduler" + }, + { + "name": "apsw", + "uri": "https://pypi.org/project/apsw" + }, + { + "name": "arabic-reshaper", + "uri": "https://pypi.org/project/arabic-reshaper" + }, + { + "name": "aresponses", + "uri": "https://pypi.org/project/aresponses" + }, + { + "name": "argcomplete", + "uri": "https://pypi.org/project/argcomplete" + }, + { + "name": "argh", + "uri": "https://pypi.org/project/argh" + }, + { + "name": "argon2-cffi", + "uri": "https://pypi.org/project/argon2-cffi" + }, + { + "name": "argon2-cffi-bindings", + "uri": "https://pypi.org/project/argon2-cffi-bindings" + }, + { + "name": "argparse", + "uri": "https://pypi.org/project/argparse" + }, + { + "name": "argparse-addons", + "uri": "https://pypi.org/project/argparse-addons" + }, + { + "name": "arnparse", + "uri": "https://pypi.org/project/arnparse" + }, + { + "name": "arpeggio", + "uri": "https://pypi.org/project/arpeggio" + }, + { + "name": "array-record", + "uri": "https://pypi.org/project/array-record" + }, + { + "name": "arrow", + "uri": "https://pypi.org/project/arrow" + }, + { + "name": "artifacts-keyring", + "uri": "https://pypi.org/project/artifacts-keyring" + }, + { + "name": "arviz", + "uri": "https://pypi.org/project/arviz" + }, + { + "name": "asana", + "uri": "https://pypi.org/project/asana" + }, + { + "name": "asciitree", + "uri": "https://pypi.org/project/asciitree" + }, + { + "name": "asgi-correlation-id", + "uri": "https://pypi.org/project/asgi-correlation-id" + }, + { + "name": "asgi-lifespan", + "uri": "https://pypi.org/project/asgi-lifespan" + }, + { + "name": "asgiref", + "uri": "https://pypi.org/project/asgiref" + }, + { + "name": "asn1crypto", + "uri": "https://pypi.org/project/asn1crypto" + }, + { + "name": "asteroid-filterbanks", + "uri": "https://pypi.org/project/asteroid-filterbanks" + }, + { + "name": "asteval", + "uri": "https://pypi.org/project/asteval" + }, + { + "name": "astor", + "uri": "https://pypi.org/project/astor" + }, + { + "name": "astral", + "uri": "https://pypi.org/project/astral" + }, + { + "name": "astroid", + "uri": "https://pypi.org/project/astroid" + }, + { + "name": "astronomer-cosmos", + "uri": "https://pypi.org/project/astronomer-cosmos" + }, + { + "name": "astropy", + "uri": "https://pypi.org/project/astropy" + }, + { + "name": "astropy-iers-data", + "uri": "https://pypi.org/project/astropy-iers-data" + }, + { + "name": "asttokens", + "uri": "https://pypi.org/project/asttokens" + }, + { + "name": "astunparse", + "uri": "https://pypi.org/project/astunparse" + }, + { + "name": "async-generator", + "uri": "https://pypi.org/project/async-generator" + }, + { + "name": "async-lru", + "uri": "https://pypi.org/project/async-lru" + }, + { + "name": "async-property", + "uri": "https://pypi.org/project/async-property" + }, + { + "name": "async-timeout", + "uri": "https://pypi.org/project/async-timeout" + }, + { + "name": "asyncio", + "uri": "https://pypi.org/project/asyncio" + }, + { + "name": "asyncpg", + "uri": "https://pypi.org/project/asyncpg" + }, + { + "name": "asyncssh", + "uri": "https://pypi.org/project/asyncssh" + }, + { + "name": "asynctest", + "uri": "https://pypi.org/project/asynctest" + }, + { + "name": "atlassian-python-api", + "uri": "https://pypi.org/project/atlassian-python-api" + }, + { + "name": "atomicwrites", + "uri": "https://pypi.org/project/atomicwrites" + }, + { + "name": "atomlite", + "uri": "https://pypi.org/project/atomlite" + }, + { + "name": "atpublic", + "uri": "https://pypi.org/project/atpublic" + }, + { + "name": "attrdict", + "uri": "https://pypi.org/project/attrdict" + }, + { + "name": "attrs", + "uri": "https://pypi.org/project/attrs" + }, + { + "name": "audioread", + "uri": "https://pypi.org/project/audioread" + }, + { + "name": "auth0-python", + "uri": "https://pypi.org/project/auth0-python" + }, + { + "name": "authencoding", + "uri": "https://pypi.org/project/authencoding" + }, + { + "name": "authlib", + "uri": "https://pypi.org/project/authlib" + }, + { + "name": "autobahn", + "uri": "https://pypi.org/project/autobahn" + }, + { + "name": "autocommand", + "uri": "https://pypi.org/project/autocommand" + }, + { + "name": "autofaker", + "uri": "https://pypi.org/project/autofaker" + }, + { + "name": "autoflake", + "uri": "https://pypi.org/project/autoflake" + }, + { + "name": "autograd", + "uri": "https://pypi.org/project/autograd" + }, + { + "name": "autograd-gamma", + "uri": "https://pypi.org/project/autograd-gamma" + }, + { + "name": "automat", + "uri": "https://pypi.org/project/automat" + }, + { + "name": "autopage", + "uri": "https://pypi.org/project/autopage" + }, + { + "name": "autopep8", + "uri": "https://pypi.org/project/autopep8" + }, + { + "name": "av", + "uri": "https://pypi.org/project/av" + }, + { + "name": "avro", + "uri": "https://pypi.org/project/avro" + }, + { + "name": "avro-gen", + "uri": "https://pypi.org/project/avro-gen" + }, + { + "name": "avro-gen3", + "uri": "https://pypi.org/project/avro-gen3" + }, + { + "name": "avro-python3", + "uri": "https://pypi.org/project/avro-python3" + }, + { + "name": "awacs", + "uri": "https://pypi.org/project/awacs" + }, + { + "name": "awesomeversion", + "uri": "https://pypi.org/project/awesomeversion" + }, + { + "name": "awkward", + "uri": "https://pypi.org/project/awkward" + }, + { + "name": "awkward-cpp", + "uri": "https://pypi.org/project/awkward-cpp" + }, + { + "name": "aws-cdk-asset-awscli-v1", + "uri": "https://pypi.org/project/aws-cdk-asset-awscli-v1" + }, + { + "name": "aws-cdk-asset-kubectl-v20", + "uri": "https://pypi.org/project/aws-cdk-asset-kubectl-v20" + }, + { + "name": "aws-cdk-asset-node-proxy-agent-v6", + "uri": "https://pypi.org/project/aws-cdk-asset-node-proxy-agent-v6" + }, + { + "name": "aws-cdk-aws-lambda-python-alpha", + "uri": "https://pypi.org/project/aws-cdk-aws-lambda-python-alpha" + }, + { + "name": "aws-cdk-cloud-assembly-schema", + "uri": "https://pypi.org/project/aws-cdk-cloud-assembly-schema" + }, + { + "name": "aws-cdk-integ-tests-alpha", + "uri": "https://pypi.org/project/aws-cdk-integ-tests-alpha" + }, + { + "name": "aws-cdk-lib", + "uri": "https://pypi.org/project/aws-cdk-lib" + }, + { + "name": "aws-encryption-sdk", + "uri": "https://pypi.org/project/aws-encryption-sdk" + }, + { + "name": "aws-lambda-builders", + "uri": "https://pypi.org/project/aws-lambda-builders" + }, + { + "name": "aws-lambda-powertools", + "uri": "https://pypi.org/project/aws-lambda-powertools" + }, + { + "name": "aws-psycopg2", + "uri": "https://pypi.org/project/aws-psycopg2" + }, + { + "name": "aws-requests-auth", + "uri": "https://pypi.org/project/aws-requests-auth" + }, + { + "name": "aws-sam-cli", + "uri": "https://pypi.org/project/aws-sam-cli" + }, + { + "name": "aws-sam-translator", + "uri": "https://pypi.org/project/aws-sam-translator" + }, + { + "name": "aws-secretsmanager-caching", + "uri": "https://pypi.org/project/aws-secretsmanager-caching" + }, + { + "name": "aws-xray-sdk", + "uri": "https://pypi.org/project/aws-xray-sdk" + }, + { + "name": "awscli", + "uri": "https://pypi.org/project/awscli" + }, + { + "name": "awscliv2", + "uri": "https://pypi.org/project/awscliv2" + }, + { + "name": "awscrt", + "uri": "https://pypi.org/project/awscrt" + }, + { + "name": "awswrangler", + "uri": "https://pypi.org/project/awswrangler" + }, + { + "name": "azure", + "uri": "https://pypi.org/project/azure" + }, + { + "name": "azure-ai-formrecognizer", + "uri": "https://pypi.org/project/azure-ai-formrecognizer" + }, + { + "name": "azure-ai-ml", + "uri": "https://pypi.org/project/azure-ai-ml" + }, + { + "name": "azure-appconfiguration", + "uri": "https://pypi.org/project/azure-appconfiguration" + }, + { + "name": "azure-applicationinsights", + "uri": "https://pypi.org/project/azure-applicationinsights" + }, + { + "name": "azure-batch", + "uri": "https://pypi.org/project/azure-batch" + }, + { + "name": "azure-cli", + "uri": "https://pypi.org/project/azure-cli" + }, + { + "name": "azure-cli-core", + "uri": "https://pypi.org/project/azure-cli-core" + }, + { + "name": "azure-cli-telemetry", + "uri": "https://pypi.org/project/azure-cli-telemetry" + }, + { + "name": "azure-common", + "uri": "https://pypi.org/project/azure-common" + }, + { + "name": "azure-core", + "uri": "https://pypi.org/project/azure-core" + }, + { + "name": "azure-core-tracing-opentelemetry", + "uri": "https://pypi.org/project/azure-core-tracing-opentelemetry" + }, + { + "name": "azure-cosmos", + "uri": "https://pypi.org/project/azure-cosmos" + }, + { + "name": "azure-cosmosdb-nspkg", + "uri": "https://pypi.org/project/azure-cosmosdb-nspkg" + }, + { + "name": "azure-cosmosdb-table", + "uri": "https://pypi.org/project/azure-cosmosdb-table" + }, + { + "name": "azure-data-tables", + "uri": "https://pypi.org/project/azure-data-tables" + }, + { + "name": "azure-datalake-store", + "uri": "https://pypi.org/project/azure-datalake-store" + }, + { + "name": "azure-devops", + "uri": "https://pypi.org/project/azure-devops" + }, + { + "name": "azure-eventgrid", + "uri": "https://pypi.org/project/azure-eventgrid" + }, + { + "name": "azure-eventhub", + "uri": "https://pypi.org/project/azure-eventhub" + }, + { + "name": "azure-functions", + "uri": "https://pypi.org/project/azure-functions" + }, + { + "name": "azure-graphrbac", + "uri": "https://pypi.org/project/azure-graphrbac" + }, + { + "name": "azure-identity", + "uri": "https://pypi.org/project/azure-identity" + }, + { + "name": "azure-keyvault", + "uri": "https://pypi.org/project/azure-keyvault" + }, + { + "name": "azure-keyvault-administration", + "uri": "https://pypi.org/project/azure-keyvault-administration" + }, + { + "name": "azure-keyvault-certificates", + "uri": "https://pypi.org/project/azure-keyvault-certificates" + }, + { + "name": "azure-keyvault-keys", + "uri": "https://pypi.org/project/azure-keyvault-keys" + }, + { + "name": "azure-keyvault-secrets", + "uri": "https://pypi.org/project/azure-keyvault-secrets" + }, + { + "name": "azure-kusto-data", + "uri": "https://pypi.org/project/azure-kusto-data" + }, + { + "name": "azure-kusto-ingest", + "uri": "https://pypi.org/project/azure-kusto-ingest" + }, + { + "name": "azure-loganalytics", + "uri": "https://pypi.org/project/azure-loganalytics" + }, + { + "name": "azure-mgmt", + "uri": "https://pypi.org/project/azure-mgmt" + }, + { + "name": "azure-mgmt-advisor", + "uri": "https://pypi.org/project/azure-mgmt-advisor" + }, + { + "name": "azure-mgmt-apimanagement", + "uri": "https://pypi.org/project/azure-mgmt-apimanagement" + }, + { + "name": "azure-mgmt-appconfiguration", + "uri": "https://pypi.org/project/azure-mgmt-appconfiguration" + }, + { + "name": "azure-mgmt-appcontainers", + "uri": "https://pypi.org/project/azure-mgmt-appcontainers" + }, + { + "name": "azure-mgmt-applicationinsights", + "uri": "https://pypi.org/project/azure-mgmt-applicationinsights" + }, + { + "name": "azure-mgmt-authorization", + "uri": "https://pypi.org/project/azure-mgmt-authorization" + }, + { + "name": "azure-mgmt-batch", + "uri": "https://pypi.org/project/azure-mgmt-batch" + }, + { + "name": "azure-mgmt-batchai", + "uri": "https://pypi.org/project/azure-mgmt-batchai" + }, + { + "name": "azure-mgmt-billing", + "uri": "https://pypi.org/project/azure-mgmt-billing" + }, + { + "name": "azure-mgmt-botservice", + "uri": "https://pypi.org/project/azure-mgmt-botservice" + }, + { + "name": "azure-mgmt-cdn", + "uri": "https://pypi.org/project/azure-mgmt-cdn" + }, + { + "name": "azure-mgmt-cognitiveservices", + "uri": "https://pypi.org/project/azure-mgmt-cognitiveservices" + }, + { + "name": "azure-mgmt-commerce", + "uri": "https://pypi.org/project/azure-mgmt-commerce" + }, + { + "name": "azure-mgmt-compute", + "uri": "https://pypi.org/project/azure-mgmt-compute" + }, + { + "name": "azure-mgmt-consumption", + "uri": "https://pypi.org/project/azure-mgmt-consumption" + }, + { + "name": "azure-mgmt-containerinstance", + "uri": "https://pypi.org/project/azure-mgmt-containerinstance" + }, + { + "name": "azure-mgmt-containerregistry", + "uri": "https://pypi.org/project/azure-mgmt-containerregistry" + }, + { + "name": "azure-mgmt-containerservice", + "uri": "https://pypi.org/project/azure-mgmt-containerservice" + }, + { + "name": "azure-mgmt-core", + "uri": "https://pypi.org/project/azure-mgmt-core" + }, + { + "name": "azure-mgmt-cosmosdb", + "uri": "https://pypi.org/project/azure-mgmt-cosmosdb" + }, + { + "name": "azure-mgmt-databoxedge", + "uri": "https://pypi.org/project/azure-mgmt-databoxedge" + }, + { + "name": "azure-mgmt-datafactory", + "uri": "https://pypi.org/project/azure-mgmt-datafactory" + }, + { + "name": "azure-mgmt-datalake-analytics", + "uri": "https://pypi.org/project/azure-mgmt-datalake-analytics" + }, + { + "name": "azure-mgmt-datalake-nspkg", + "uri": "https://pypi.org/project/azure-mgmt-datalake-nspkg" + }, + { + "name": "azure-mgmt-datalake-store", + "uri": "https://pypi.org/project/azure-mgmt-datalake-store" + }, + { + "name": "azure-mgmt-datamigration", + "uri": "https://pypi.org/project/azure-mgmt-datamigration" + }, + { + "name": "azure-mgmt-deploymentmanager", + "uri": "https://pypi.org/project/azure-mgmt-deploymentmanager" + }, + { + "name": "azure-mgmt-devspaces", + "uri": "https://pypi.org/project/azure-mgmt-devspaces" + }, + { + "name": "azure-mgmt-devtestlabs", + "uri": "https://pypi.org/project/azure-mgmt-devtestlabs" + }, + { + "name": "azure-mgmt-dns", + "uri": "https://pypi.org/project/azure-mgmt-dns" + }, + { + "name": "azure-mgmt-eventgrid", + "uri": "https://pypi.org/project/azure-mgmt-eventgrid" + }, + { + "name": "azure-mgmt-eventhub", + "uri": "https://pypi.org/project/azure-mgmt-eventhub" + }, + { + "name": "azure-mgmt-extendedlocation", + "uri": "https://pypi.org/project/azure-mgmt-extendedlocation" + }, + { + "name": "azure-mgmt-hanaonazure", + "uri": "https://pypi.org/project/azure-mgmt-hanaonazure" + }, + { + "name": "azure-mgmt-hdinsight", + "uri": "https://pypi.org/project/azure-mgmt-hdinsight" + }, + { + "name": "azure-mgmt-imagebuilder", + "uri": "https://pypi.org/project/azure-mgmt-imagebuilder" + }, + { + "name": "azure-mgmt-iotcentral", + "uri": "https://pypi.org/project/azure-mgmt-iotcentral" + }, + { + "name": "azure-mgmt-iothub", + "uri": "https://pypi.org/project/azure-mgmt-iothub" + }, + { + "name": "azure-mgmt-iothubprovisioningservices", + "uri": "https://pypi.org/project/azure-mgmt-iothubprovisioningservices" + }, + { + "name": "azure-mgmt-keyvault", + "uri": "https://pypi.org/project/azure-mgmt-keyvault" + }, + { + "name": "azure-mgmt-kusto", + "uri": "https://pypi.org/project/azure-mgmt-kusto" + }, + { + "name": "azure-mgmt-loganalytics", + "uri": "https://pypi.org/project/azure-mgmt-loganalytics" + }, + { + "name": "azure-mgmt-logic", + "uri": "https://pypi.org/project/azure-mgmt-logic" + }, + { + "name": "azure-mgmt-machinelearningcompute", + "uri": "https://pypi.org/project/azure-mgmt-machinelearningcompute" + }, + { + "name": "azure-mgmt-managedservices", + "uri": "https://pypi.org/project/azure-mgmt-managedservices" + }, + { + "name": "azure-mgmt-managementgroups", + "uri": "https://pypi.org/project/azure-mgmt-managementgroups" + }, + { + "name": "azure-mgmt-managementpartner", + "uri": "https://pypi.org/project/azure-mgmt-managementpartner" + }, + { + "name": "azure-mgmt-maps", + "uri": "https://pypi.org/project/azure-mgmt-maps" + }, + { + "name": "azure-mgmt-marketplaceordering", + "uri": "https://pypi.org/project/azure-mgmt-marketplaceordering" + }, + { + "name": "azure-mgmt-media", + "uri": "https://pypi.org/project/azure-mgmt-media" + }, + { + "name": "azure-mgmt-monitor", + "uri": "https://pypi.org/project/azure-mgmt-monitor" + }, + { + "name": "azure-mgmt-msi", + "uri": "https://pypi.org/project/azure-mgmt-msi" + }, + { + "name": "azure-mgmt-netapp", + "uri": "https://pypi.org/project/azure-mgmt-netapp" + }, + { + "name": "azure-mgmt-network", + "uri": "https://pypi.org/project/azure-mgmt-network" + }, + { + "name": "azure-mgmt-notificationhubs", + "uri": "https://pypi.org/project/azure-mgmt-notificationhubs" + }, + { + "name": "azure-mgmt-nspkg", + "uri": "https://pypi.org/project/azure-mgmt-nspkg" + }, + { + "name": "azure-mgmt-policyinsights", + "uri": "https://pypi.org/project/azure-mgmt-policyinsights" + }, + { + "name": "azure-mgmt-powerbiembedded", + "uri": "https://pypi.org/project/azure-mgmt-powerbiembedded" + }, + { + "name": "azure-mgmt-privatedns", + "uri": "https://pypi.org/project/azure-mgmt-privatedns" + }, + { + "name": "azure-mgmt-rdbms", + "uri": "https://pypi.org/project/azure-mgmt-rdbms" + }, + { + "name": "azure-mgmt-recoveryservices", + "uri": "https://pypi.org/project/azure-mgmt-recoveryservices" + }, + { + "name": "azure-mgmt-recoveryservicesbackup", + "uri": "https://pypi.org/project/azure-mgmt-recoveryservicesbackup" + }, + { + "name": "azure-mgmt-redhatopenshift", + "uri": "https://pypi.org/project/azure-mgmt-redhatopenshift" + }, + { + "name": "azure-mgmt-redis", + "uri": "https://pypi.org/project/azure-mgmt-redis" + }, + { + "name": "azure-mgmt-relay", + "uri": "https://pypi.org/project/azure-mgmt-relay" + }, + { + "name": "azure-mgmt-reservations", + "uri": "https://pypi.org/project/azure-mgmt-reservations" + }, + { + "name": "azure-mgmt-resource", + "uri": "https://pypi.org/project/azure-mgmt-resource" + }, + { + "name": "azure-mgmt-resourcegraph", + "uri": "https://pypi.org/project/azure-mgmt-resourcegraph" + }, + { + "name": "azure-mgmt-scheduler", + "uri": "https://pypi.org/project/azure-mgmt-scheduler" + }, + { + "name": "azure-mgmt-search", + "uri": "https://pypi.org/project/azure-mgmt-search" + }, + { + "name": "azure-mgmt-security", + "uri": "https://pypi.org/project/azure-mgmt-security" + }, + { + "name": "azure-mgmt-servicebus", + "uri": "https://pypi.org/project/azure-mgmt-servicebus" + }, + { + "name": "azure-mgmt-servicefabric", + "uri": "https://pypi.org/project/azure-mgmt-servicefabric" + }, + { + "name": "azure-mgmt-servicefabricmanagedclusters", + "uri": "https://pypi.org/project/azure-mgmt-servicefabricmanagedclusters" + }, + { + "name": "azure-mgmt-servicelinker", + "uri": "https://pypi.org/project/azure-mgmt-servicelinker" + }, + { + "name": "azure-mgmt-signalr", + "uri": "https://pypi.org/project/azure-mgmt-signalr" + }, + { + "name": "azure-mgmt-sql", + "uri": "https://pypi.org/project/azure-mgmt-sql" + }, + { + "name": "azure-mgmt-sqlvirtualmachine", + "uri": "https://pypi.org/project/azure-mgmt-sqlvirtualmachine" + }, + { + "name": "azure-mgmt-storage", + "uri": "https://pypi.org/project/azure-mgmt-storage" + }, + { + "name": "azure-mgmt-subscription", + "uri": "https://pypi.org/project/azure-mgmt-subscription" + }, + { + "name": "azure-mgmt-synapse", + "uri": "https://pypi.org/project/azure-mgmt-synapse" + }, + { + "name": "azure-mgmt-trafficmanager", + "uri": "https://pypi.org/project/azure-mgmt-trafficmanager" + }, + { + "name": "azure-mgmt-web", + "uri": "https://pypi.org/project/azure-mgmt-web" + }, + { + "name": "azure-monitor-opentelemetry", + "uri": "https://pypi.org/project/azure-monitor-opentelemetry" + }, + { + "name": "azure-monitor-opentelemetry-exporter", + "uri": "https://pypi.org/project/azure-monitor-opentelemetry-exporter" + }, + { + "name": "azure-monitor-query", + "uri": "https://pypi.org/project/azure-monitor-query" + }, + { + "name": "azure-multiapi-storage", + "uri": "https://pypi.org/project/azure-multiapi-storage" + }, + { + "name": "azure-nspkg", + "uri": "https://pypi.org/project/azure-nspkg" + }, + { + "name": "azure-search-documents", + "uri": "https://pypi.org/project/azure-search-documents" + }, + { + "name": "azure-servicebus", + "uri": "https://pypi.org/project/azure-servicebus" + }, + { + "name": "azure-servicefabric", + "uri": "https://pypi.org/project/azure-servicefabric" + }, + { + "name": "azure-servicemanagement-legacy", + "uri": "https://pypi.org/project/azure-servicemanagement-legacy" + }, + { + "name": "azure-storage", + "uri": "https://pypi.org/project/azure-storage" + }, + { + "name": "azure-storage-blob", + "uri": "https://pypi.org/project/azure-storage-blob" + }, + { + "name": "azure-storage-common", + "uri": "https://pypi.org/project/azure-storage-common" + }, + { + "name": "azure-storage-file", + "uri": "https://pypi.org/project/azure-storage-file" + }, + { + "name": "azure-storage-file-datalake", + "uri": "https://pypi.org/project/azure-storage-file-datalake" + }, + { + "name": "azure-storage-file-share", + "uri": "https://pypi.org/project/azure-storage-file-share" + }, + { + "name": "azure-storage-nspkg", + "uri": "https://pypi.org/project/azure-storage-nspkg" + }, + { + "name": "azure-storage-queue", + "uri": "https://pypi.org/project/azure-storage-queue" + }, + { + "name": "azure-synapse-accesscontrol", + "uri": "https://pypi.org/project/azure-synapse-accesscontrol" + }, + { + "name": "azure-synapse-artifacts", + "uri": "https://pypi.org/project/azure-synapse-artifacts" + }, + { + "name": "azure-synapse-managedprivateendpoints", + "uri": "https://pypi.org/project/azure-synapse-managedprivateendpoints" + }, + { + "name": "azure-synapse-spark", + "uri": "https://pypi.org/project/azure-synapse-spark" + }, + { + "name": "azureml-core", + "uri": "https://pypi.org/project/azureml-core" + }, + { + "name": "azureml-dataprep", + "uri": "https://pypi.org/project/azureml-dataprep" + }, + { + "name": "azureml-dataprep-native", + "uri": "https://pypi.org/project/azureml-dataprep-native" + }, + { + "name": "azureml-dataprep-rslex", + "uri": "https://pypi.org/project/azureml-dataprep-rslex" + }, + { + "name": "azureml-dataset-runtime", + "uri": "https://pypi.org/project/azureml-dataset-runtime" + }, + { + "name": "azureml-mlflow", + "uri": "https://pypi.org/project/azureml-mlflow" + }, + { + "name": "azureml-telemetry", + "uri": "https://pypi.org/project/azureml-telemetry" + }, + { + "name": "babel", + "uri": "https://pypi.org/project/babel" + }, + { + "name": "backcall", + "uri": "https://pypi.org/project/backcall" + }, + { + "name": "backoff", + "uri": "https://pypi.org/project/backoff" + }, + { + "name": "backports-abc", + "uri": "https://pypi.org/project/backports-abc" + }, + { + "name": "backports-cached-property", + "uri": "https://pypi.org/project/backports-cached-property" + }, + { + "name": "backports-csv", + "uri": "https://pypi.org/project/backports-csv" + }, + { + "name": "backports-datetime-fromisoformat", + "uri": "https://pypi.org/project/backports-datetime-fromisoformat" + }, + { + "name": "backports-entry-points-selectable", + "uri": "https://pypi.org/project/backports-entry-points-selectable" + }, + { + "name": "backports-functools-lru-cache", + "uri": "https://pypi.org/project/backports-functools-lru-cache" + }, + { + "name": "backports-shutil-get-terminal-size", + "uri": "https://pypi.org/project/backports-shutil-get-terminal-size" + }, + { + "name": "backports-tarfile", + "uri": "https://pypi.org/project/backports-tarfile" + }, + { + "name": "backports-tempfile", + "uri": "https://pypi.org/project/backports-tempfile" + }, + { + "name": "backports-weakref", + "uri": "https://pypi.org/project/backports-weakref" + }, + { + "name": "backports-zoneinfo", + "uri": "https://pypi.org/project/backports-zoneinfo" + }, + { + "name": "bandit", + "uri": "https://pypi.org/project/bandit" + }, + { + "name": "base58", + "uri": "https://pypi.org/project/base58" + }, + { + "name": "bashlex", + "uri": "https://pypi.org/project/bashlex" + }, + { + "name": "bazel-runfiles", + "uri": "https://pypi.org/project/bazel-runfiles" + }, + { + "name": "bc-detect-secrets", + "uri": "https://pypi.org/project/bc-detect-secrets" + }, + { + "name": "bc-jsonpath-ng", + "uri": "https://pypi.org/project/bc-jsonpath-ng" + }, + { + "name": "bc-python-hcl2", + "uri": "https://pypi.org/project/bc-python-hcl2" + }, + { + "name": "bcrypt", + "uri": "https://pypi.org/project/bcrypt" + }, + { + "name": "beartype", + "uri": "https://pypi.org/project/beartype" + }, + { + "name": "beautifulsoup", + "uri": "https://pypi.org/project/beautifulsoup" + }, + { + "name": "beautifulsoup4", + "uri": "https://pypi.org/project/beautifulsoup4" + }, + { + "name": "behave", + "uri": "https://pypi.org/project/behave" + }, + { + "name": "bellows", + "uri": "https://pypi.org/project/bellows" + }, + { + "name": "betterproto", + "uri": "https://pypi.org/project/betterproto" + }, + { + "name": "bidict", + "uri": "https://pypi.org/project/bidict" + }, + { + "name": "billiard", + "uri": "https://pypi.org/project/billiard" + }, + { + "name": "binaryornot", + "uri": "https://pypi.org/project/binaryornot" + }, + { + "name": "bio", + "uri": "https://pypi.org/project/bio" + }, + { + "name": "biopython", + "uri": "https://pypi.org/project/biopython" + }, + { + "name": "biothings-client", + "uri": "https://pypi.org/project/biothings-client" + }, + { + "name": "bitarray", + "uri": "https://pypi.org/project/bitarray" + }, + { + "name": "bitsandbytes", + "uri": "https://pypi.org/project/bitsandbytes" + }, + { + "name": "bitstring", + "uri": "https://pypi.org/project/bitstring" + }, + { + "name": "bitstruct", + "uri": "https://pypi.org/project/bitstruct" + }, + { + "name": "black", + "uri": "https://pypi.org/project/black" + }, + { + "name": "blackduck", + "uri": "https://pypi.org/project/blackduck" + }, + { + "name": "bleach", + "uri": "https://pypi.org/project/bleach" + }, + { + "name": "bleak", + "uri": "https://pypi.org/project/bleak" + }, + { + "name": "blendmodes", + "uri": "https://pypi.org/project/blendmodes" + }, + { + "name": "blessed", + "uri": "https://pypi.org/project/blessed" + }, + { + "name": "blessings", + "uri": "https://pypi.org/project/blessings" + }, + { + "name": "blinker", + "uri": "https://pypi.org/project/blinker" + }, + { + "name": "blis", + "uri": "https://pypi.org/project/blis" + }, + { + "name": "blobfile", + "uri": "https://pypi.org/project/blobfile" + }, + { + "name": "blosc2", + "uri": "https://pypi.org/project/blosc2" + }, + { + "name": "boa-str", + "uri": "https://pypi.org/project/boa-str" + }, + { + "name": "bokeh", + "uri": "https://pypi.org/project/bokeh" + }, + { + "name": "boltons", + "uri": "https://pypi.org/project/boltons" + }, + { + "name": "boolean-py", + "uri": "https://pypi.org/project/boolean-py" + }, + { + "name": "boto", + "uri": "https://pypi.org/project/boto" + }, + { + "name": "boto3", + "uri": "https://pypi.org/project/boto3" + }, + { + "name": "boto3-stubs", + "uri": "https://pypi.org/project/boto3-stubs" + }, + { + "name": "boto3-stubs-lite", + "uri": "https://pypi.org/project/boto3-stubs-lite" + }, + { + "name": "boto3-type-annotations", + "uri": "https://pypi.org/project/boto3-type-annotations" + }, + { + "name": "botocore", + "uri": "https://pypi.org/project/botocore" + }, + { + "name": "botocore-stubs", + "uri": "https://pypi.org/project/botocore-stubs" + }, + { + "name": "bottle", + "uri": "https://pypi.org/project/bottle" + }, + { + "name": "bottleneck", + "uri": "https://pypi.org/project/bottleneck" + }, + { + "name": "boxsdk", + "uri": "https://pypi.org/project/boxsdk" + }, + { + "name": "braceexpand", + "uri": "https://pypi.org/project/braceexpand" + }, + { + "name": "bracex", + "uri": "https://pypi.org/project/bracex" + }, + { + "name": "branca", + "uri": "https://pypi.org/project/branca" + }, + { + "name": "breathe", + "uri": "https://pypi.org/project/breathe" + }, + { + "name": "brotli", + "uri": "https://pypi.org/project/brotli" + }, + { + "name": "bs4", + "uri": "https://pypi.org/project/bs4" + }, + { + "name": "bson", + "uri": "https://pypi.org/project/bson" + }, + { + "name": "btrees", + "uri": "https://pypi.org/project/btrees" + }, + { + "name": "build", + "uri": "https://pypi.org/project/build" + }, + { + "name": "bump2version", + "uri": "https://pypi.org/project/bump2version" + }, + { + "name": "bumpversion", + "uri": "https://pypi.org/project/bumpversion" + }, + { + "name": "bytecode", + "uri": "https://pypi.org/project/bytecode" + }, + { + "name": "bz2file", + "uri": "https://pypi.org/project/bz2file" + }, + { + "name": "c7n", + "uri": "https://pypi.org/project/c7n" + }, + { + "name": "c7n-org", + "uri": "https://pypi.org/project/c7n-org" + }, + { + "name": "cachecontrol", + "uri": "https://pypi.org/project/cachecontrol" + }, + { + "name": "cached-property", + "uri": "https://pypi.org/project/cached-property" + }, + { + "name": "cachelib", + "uri": "https://pypi.org/project/cachelib" + }, + { + "name": "cachetools", + "uri": "https://pypi.org/project/cachetools" + }, + { + "name": "cachy", + "uri": "https://pypi.org/project/cachy" + }, + { + "name": "cairocffi", + "uri": "https://pypi.org/project/cairocffi" + }, + { + "name": "cairosvg", + "uri": "https://pypi.org/project/cairosvg" + }, + { + "name": "casadi", + "uri": "https://pypi.org/project/casadi" + }, + { + "name": "case-conversion", + "uri": "https://pypi.org/project/case-conversion" + }, + { + "name": "cassandra-driver", + "uri": "https://pypi.org/project/cassandra-driver" + }, + { + "name": "catalogue", + "uri": "https://pypi.org/project/catalogue" + }, + { + "name": "catboost", + "uri": "https://pypi.org/project/catboost" + }, + { + "name": "category-encoders", + "uri": "https://pypi.org/project/category-encoders" + }, + { + "name": "cattrs", + "uri": "https://pypi.org/project/cattrs" + }, + { + "name": "cbor", + "uri": "https://pypi.org/project/cbor" + }, + { + "name": "cbor2", + "uri": "https://pypi.org/project/cbor2" + }, + { + "name": "cchardet", + "uri": "https://pypi.org/project/cchardet" + }, + { + "name": "ccxt", + "uri": "https://pypi.org/project/ccxt" + }, + { + "name": "cdk-nag", + "uri": "https://pypi.org/project/cdk-nag" + }, + { + "name": "celery", + "uri": "https://pypi.org/project/celery" + }, + { + "name": "cerberus", + "uri": "https://pypi.org/project/cerberus" + }, + { + "name": "cerberus-python-client", + "uri": "https://pypi.org/project/cerberus-python-client" + }, + { + "name": "certbot", + "uri": "https://pypi.org/project/certbot" + }, + { + "name": "certbot-dns-cloudflare", + "uri": "https://pypi.org/project/certbot-dns-cloudflare" + }, + { + "name": "certifi", + "uri": "https://pypi.org/project/certifi" + }, + { + "name": "cffi", + "uri": "https://pypi.org/project/cffi" + }, + { + "name": "cfgv", + "uri": "https://pypi.org/project/cfgv" + }, + { + "name": "cfile", + "uri": "https://pypi.org/project/cfile" + }, + { + "name": "cfn-flip", + "uri": "https://pypi.org/project/cfn-flip" + }, + { + "name": "cfn-lint", + "uri": "https://pypi.org/project/cfn-lint" + }, + { + "name": "cftime", + "uri": "https://pypi.org/project/cftime" + }, + { + "name": "chameleon", + "uri": "https://pypi.org/project/chameleon" + }, + { + "name": "channels", + "uri": "https://pypi.org/project/channels" + }, + { + "name": "channels-redis", + "uri": "https://pypi.org/project/channels-redis" + }, + { + "name": "chardet", + "uri": "https://pypi.org/project/chardet" + }, + { + "name": "charset-normalizer", + "uri": "https://pypi.org/project/charset-normalizer" + }, + { + "name": "checkdigit", + "uri": "https://pypi.org/project/checkdigit" + }, + { + "name": "checkov", + "uri": "https://pypi.org/project/checkov" + }, + { + "name": "checksumdir", + "uri": "https://pypi.org/project/checksumdir" + }, + { + "name": "cheroot", + "uri": "https://pypi.org/project/cheroot" + }, + { + "name": "cherrypy", + "uri": "https://pypi.org/project/cherrypy" + }, + { + "name": "chevron", + "uri": "https://pypi.org/project/chevron" + }, + { + "name": "chex", + "uri": "https://pypi.org/project/chex" + }, + { + "name": "chispa", + "uri": "https://pypi.org/project/chispa" + }, + { + "name": "chroma-hnswlib", + "uri": "https://pypi.org/project/chroma-hnswlib" + }, + { + "name": "chromadb", + "uri": "https://pypi.org/project/chromadb" + }, + { + "name": "cibuildwheel", + "uri": "https://pypi.org/project/cibuildwheel" + }, + { + "name": "cinemagoer", + "uri": "https://pypi.org/project/cinemagoer" + }, + { + "name": "circuitbreaker", + "uri": "https://pypi.org/project/circuitbreaker" + }, + { + "name": "ciso8601", + "uri": "https://pypi.org/project/ciso8601" + }, + { + "name": "ckzg", + "uri": "https://pypi.org/project/ckzg" + }, + { + "name": "clang", + "uri": "https://pypi.org/project/clang" + }, + { + "name": "clang-format", + "uri": "https://pypi.org/project/clang-format" + }, + { + "name": "clarabel", + "uri": "https://pypi.org/project/clarabel" + }, + { + "name": "clean-fid", + "uri": "https://pypi.org/project/clean-fid" + }, + { + "name": "cleanco", + "uri": "https://pypi.org/project/cleanco" + }, + { + "name": "cleo", + "uri": "https://pypi.org/project/cleo" + }, + { + "name": "click", + "uri": "https://pypi.org/project/click" + }, + { + "name": "click-default-group", + "uri": "https://pypi.org/project/click-default-group" + }, + { + "name": "click-didyoumean", + "uri": "https://pypi.org/project/click-didyoumean" + }, + { + "name": "click-help-colors", + "uri": "https://pypi.org/project/click-help-colors" + }, + { + "name": "click-log", + "uri": "https://pypi.org/project/click-log" + }, + { + "name": "click-man", + "uri": "https://pypi.org/project/click-man" + }, + { + "name": "click-option-group", + "uri": "https://pypi.org/project/click-option-group" + }, + { + "name": "click-plugins", + "uri": "https://pypi.org/project/click-plugins" + }, + { + "name": "click-repl", + "uri": "https://pypi.org/project/click-repl" + }, + { + "name": "click-spinner", + "uri": "https://pypi.org/project/click-spinner" + }, + { + "name": "clickclick", + "uri": "https://pypi.org/project/clickclick" + }, + { + "name": "clickhouse-connect", + "uri": "https://pypi.org/project/clickhouse-connect" + }, + { + "name": "clickhouse-driver", + "uri": "https://pypi.org/project/clickhouse-driver" + }, + { + "name": "cliff", + "uri": "https://pypi.org/project/cliff" + }, + { + "name": "cligj", + "uri": "https://pypi.org/project/cligj" + }, + { + "name": "clikit", + "uri": "https://pypi.org/project/clikit" + }, + { + "name": "clipboard", + "uri": "https://pypi.org/project/clipboard" + }, + { + "name": "cloud-sql-python-connector", + "uri": "https://pypi.org/project/cloud-sql-python-connector" + }, + { + "name": "cloudevents", + "uri": "https://pypi.org/project/cloudevents" + }, + { + "name": "cloudflare", + "uri": "https://pypi.org/project/cloudflare" + }, + { + "name": "cloudpathlib", + "uri": "https://pypi.org/project/cloudpathlib" + }, + { + "name": "cloudpickle", + "uri": "https://pypi.org/project/cloudpickle" + }, + { + "name": "cloudscraper", + "uri": "https://pypi.org/project/cloudscraper" + }, + { + "name": "cloudsplaining", + "uri": "https://pypi.org/project/cloudsplaining" + }, + { + "name": "cmaes", + "uri": "https://pypi.org/project/cmaes" + }, + { + "name": "cmake", + "uri": "https://pypi.org/project/cmake" + }, + { + "name": "cmd2", + "uri": "https://pypi.org/project/cmd2" + }, + { + "name": "cmdstanpy", + "uri": "https://pypi.org/project/cmdstanpy" + }, + { + "name": "cmudict", + "uri": "https://pypi.org/project/cmudict" + }, + { + "name": "cobble", + "uri": "https://pypi.org/project/cobble" + }, + { + "name": "codecov", + "uri": "https://pypi.org/project/codecov" + }, + { + "name": "codeowners", + "uri": "https://pypi.org/project/codeowners" + }, + { + "name": "codespell", + "uri": "https://pypi.org/project/codespell" + }, + { + "name": "cog", + "uri": "https://pypi.org/project/cog" + }, + { + "name": "cohere", + "uri": "https://pypi.org/project/cohere" + }, + { + "name": "collections-extended", + "uri": "https://pypi.org/project/collections-extended" + }, + { + "name": "colorama", + "uri": "https://pypi.org/project/colorama" + }, + { + "name": "colorcet", + "uri": "https://pypi.org/project/colorcet" + }, + { + "name": "colorclass", + "uri": "https://pypi.org/project/colorclass" + }, + { + "name": "colored", + "uri": "https://pypi.org/project/colored" + }, + { + "name": "coloredlogs", + "uri": "https://pypi.org/project/coloredlogs" + }, + { + "name": "colorful", + "uri": "https://pypi.org/project/colorful" + }, + { + "name": "colorlog", + "uri": "https://pypi.org/project/colorlog" + }, + { + "name": "colorzero", + "uri": "https://pypi.org/project/colorzero" + }, + { + "name": "colour", + "uri": "https://pypi.org/project/colour" + }, + { + "name": "comm", + "uri": "https://pypi.org/project/comm" + }, + { + "name": "commentjson", + "uri": "https://pypi.org/project/commentjson" + }, + { + "name": "commonmark", + "uri": "https://pypi.org/project/commonmark" + }, + { + "name": "comtypes", + "uri": "https://pypi.org/project/comtypes" + }, + { + "name": "conan", + "uri": "https://pypi.org/project/conan" + }, + { + "name": "concurrent-log-handler", + "uri": "https://pypi.org/project/concurrent-log-handler" + }, + { + "name": "confection", + "uri": "https://pypi.org/project/confection" + }, + { + "name": "config", + "uri": "https://pypi.org/project/config" + }, + { + "name": "configargparse", + "uri": "https://pypi.org/project/configargparse" + }, + { + "name": "configobj", + "uri": "https://pypi.org/project/configobj" + }, + { + "name": "configparser", + "uri": "https://pypi.org/project/configparser" + }, + { + "name": "configupdater", + "uri": "https://pypi.org/project/configupdater" + }, + { + "name": "confluent-kafka", + "uri": "https://pypi.org/project/confluent-kafka" + }, + { + "name": "connexion", + "uri": "https://pypi.org/project/connexion" + }, + { + "name": "constantly", + "uri": "https://pypi.org/project/constantly" + }, + { + "name": "construct", + "uri": "https://pypi.org/project/construct" + }, + { + "name": "constructs", + "uri": "https://pypi.org/project/constructs" + }, + { + "name": "contextlib2", + "uri": "https://pypi.org/project/contextlib2" + }, + { + "name": "contextvars", + "uri": "https://pypi.org/project/contextvars" + }, + { + "name": "contourpy", + "uri": "https://pypi.org/project/contourpy" + }, + { + "name": "convertdate", + "uri": "https://pypi.org/project/convertdate" + }, + { + "name": "cookiecutter", + "uri": "https://pypi.org/project/cookiecutter" + }, + { + "name": "coolname", + "uri": "https://pypi.org/project/coolname" + }, + { + "name": "core-universal", + "uri": "https://pypi.org/project/core-universal" + }, + { + "name": "coreapi", + "uri": "https://pypi.org/project/coreapi" + }, + { + "name": "coreschema", + "uri": "https://pypi.org/project/coreschema" + }, + { + "name": "corner", + "uri": "https://pypi.org/project/corner" + }, + { + "name": "country-converter", + "uri": "https://pypi.org/project/country-converter" + }, + { + "name": "courlan", + "uri": "https://pypi.org/project/courlan" + }, + { + "name": "coverage", + "uri": "https://pypi.org/project/coverage" + }, + { + "name": "coveralls", + "uri": "https://pypi.org/project/coveralls" + }, + { + "name": "cramjam", + "uri": "https://pypi.org/project/cramjam" + }, + { + "name": "crashtest", + "uri": "https://pypi.org/project/crashtest" + }, + { + "name": "crayons", + "uri": "https://pypi.org/project/crayons" + }, + { + "name": "crc32c", + "uri": "https://pypi.org/project/crc32c" + }, + { + "name": "crccheck", + "uri": "https://pypi.org/project/crccheck" + }, + { + "name": "crcmod", + "uri": "https://pypi.org/project/crcmod" + }, + { + "name": "credstash", + "uri": "https://pypi.org/project/credstash" + }, + { + "name": "crispy-bootstrap5", + "uri": "https://pypi.org/project/crispy-bootstrap5" + }, + { + "name": "cron-descriptor", + "uri": "https://pypi.org/project/cron-descriptor" + }, + { + "name": "croniter", + "uri": "https://pypi.org/project/croniter" + }, + { + "name": "crypto", + "uri": "https://pypi.org/project/crypto" + }, + { + "name": "cryptography", + "uri": "https://pypi.org/project/cryptography" + }, + { + "name": "cssselect", + "uri": "https://pypi.org/project/cssselect" + }, + { + "name": "cssselect2", + "uri": "https://pypi.org/project/cssselect2" + }, + { + "name": "cssutils", + "uri": "https://pypi.org/project/cssutils" + }, + { + "name": "ctranslate2", + "uri": "https://pypi.org/project/ctranslate2" + }, + { + "name": "cuda-python", + "uri": "https://pypi.org/project/cuda-python" + }, + { + "name": "curl-cffi", + "uri": "https://pypi.org/project/curl-cffi" + }, + { + "name": "curlify", + "uri": "https://pypi.org/project/curlify" + }, + { + "name": "cursor", + "uri": "https://pypi.org/project/cursor" + }, + { + "name": "custom-inherit", + "uri": "https://pypi.org/project/custom-inherit" + }, + { + "name": "customtkinter", + "uri": "https://pypi.org/project/customtkinter" + }, + { + "name": "cvdupdate", + "uri": "https://pypi.org/project/cvdupdate" + }, + { + "name": "cvxopt", + "uri": "https://pypi.org/project/cvxopt" + }, + { + "name": "cvxpy", + "uri": "https://pypi.org/project/cvxpy" + }, + { + "name": "cx-oracle", + "uri": "https://pypi.org/project/cx-oracle" + }, + { + "name": "cycler", + "uri": "https://pypi.org/project/cycler" + }, + { + "name": "cyclonedx-python-lib", + "uri": "https://pypi.org/project/cyclonedx-python-lib" + }, + { + "name": "cymem", + "uri": "https://pypi.org/project/cymem" + }, + { + "name": "cython", + "uri": "https://pypi.org/project/cython" + }, + { + "name": "cytoolz", + "uri": "https://pypi.org/project/cytoolz" + }, + { + "name": "dacite", + "uri": "https://pypi.org/project/dacite" + }, + { + "name": "daff", + "uri": "https://pypi.org/project/daff" + }, + { + "name": "dagster", + "uri": "https://pypi.org/project/dagster" + }, + { + "name": "dagster-aws", + "uri": "https://pypi.org/project/dagster-aws" + }, + { + "name": "dagster-graphql", + "uri": "https://pypi.org/project/dagster-graphql" + }, + { + "name": "dagster-pipes", + "uri": "https://pypi.org/project/dagster-pipes" + }, + { + "name": "dagster-postgres", + "uri": "https://pypi.org/project/dagster-postgres" + }, + { + "name": "dagster-webserver", + "uri": "https://pypi.org/project/dagster-webserver" + }, + { + "name": "daphne", + "uri": "https://pypi.org/project/daphne" + }, + { + "name": "darkdetect", + "uri": "https://pypi.org/project/darkdetect" + }, + { + "name": "dash", + "uri": "https://pypi.org/project/dash" + }, + { + "name": "dash-bootstrap-components", + "uri": "https://pypi.org/project/dash-bootstrap-components" + }, + { + "name": "dash-core-components", + "uri": "https://pypi.org/project/dash-core-components" + }, + { + "name": "dash-html-components", + "uri": "https://pypi.org/project/dash-html-components" + }, + { + "name": "dash-table", + "uri": "https://pypi.org/project/dash-table" + }, + { + "name": "dask", + "uri": "https://pypi.org/project/dask" + }, + { + "name": "dask-expr", + "uri": "https://pypi.org/project/dask-expr" + }, + { + "name": "databases", + "uri": "https://pypi.org/project/databases" + }, + { + "name": "databend-driver", + "uri": "https://pypi.org/project/databend-driver" + }, + { + "name": "databend-py", + "uri": "https://pypi.org/project/databend-py" + }, + { + "name": "databricks", + "uri": "https://pypi.org/project/databricks" + }, + { + "name": "databricks-api", + "uri": "https://pypi.org/project/databricks-api" + }, + { + "name": "databricks-cli", + "uri": "https://pypi.org/project/databricks-cli" + }, + { + "name": "databricks-connect", + "uri": "https://pypi.org/project/databricks-connect" + }, + { + "name": "databricks-feature-store", + "uri": "https://pypi.org/project/databricks-feature-store" + }, + { + "name": "databricks-pypi1", + "uri": "https://pypi.org/project/databricks-pypi1" + }, + { + "name": "databricks-pypi2", + "uri": "https://pypi.org/project/databricks-pypi2" + }, + { + "name": "databricks-sdk", + "uri": "https://pypi.org/project/databricks-sdk" + }, + { + "name": "databricks-sql-connector", + "uri": "https://pypi.org/project/databricks-sql-connector" + }, + { + "name": "dataclasses", + "uri": "https://pypi.org/project/dataclasses" + }, + { + "name": "dataclasses-json", + "uri": "https://pypi.org/project/dataclasses-json" + }, + { + "name": "datacompy", + "uri": "https://pypi.org/project/datacompy" + }, + { + "name": "datadog", + "uri": "https://pypi.org/project/datadog" + }, + { + "name": "datadog-api-client", + "uri": "https://pypi.org/project/datadog-api-client" + }, + { + "name": "datadog-logger", + "uri": "https://pypi.org/project/datadog-logger" + }, + { + "name": "datamodel-code-generator", + "uri": "https://pypi.org/project/datamodel-code-generator" + }, + { + "name": "dataproperty", + "uri": "https://pypi.org/project/dataproperty" + }, + { + "name": "datasets", + "uri": "https://pypi.org/project/datasets" + }, + { + "name": "datasketch", + "uri": "https://pypi.org/project/datasketch" + }, + { + "name": "datefinder", + "uri": "https://pypi.org/project/datefinder" + }, + { + "name": "dateformat", + "uri": "https://pypi.org/project/dateformat" + }, + { + "name": "dateparser", + "uri": "https://pypi.org/project/dateparser" + }, + { + "name": "datetime", + "uri": "https://pypi.org/project/datetime" + }, + { + "name": "dateutils", + "uri": "https://pypi.org/project/dateutils" + }, + { + "name": "db-contrib-tool", + "uri": "https://pypi.org/project/db-contrib-tool" + }, + { + "name": "db-dtypes", + "uri": "https://pypi.org/project/db-dtypes" + }, + { + "name": "dbfread", + "uri": "https://pypi.org/project/dbfread" + }, + { + "name": "dbl-tempo", + "uri": "https://pypi.org/project/dbl-tempo" + }, + { + "name": "dbt-adapters", + "uri": "https://pypi.org/project/dbt-adapters" + }, + { + "name": "dbt-bigquery", + "uri": "https://pypi.org/project/dbt-bigquery" + }, + { + "name": "dbt-common", + "uri": "https://pypi.org/project/dbt-common" + }, + { + "name": "dbt-core", + "uri": "https://pypi.org/project/dbt-core" + }, + { + "name": "dbt-databricks", + "uri": "https://pypi.org/project/dbt-databricks" + }, + { + "name": "dbt-extractor", + "uri": "https://pypi.org/project/dbt-extractor" + }, + { + "name": "dbt-postgres", + "uri": "https://pypi.org/project/dbt-postgres" + }, + { + "name": "dbt-redshift", + "uri": "https://pypi.org/project/dbt-redshift" + }, + { + "name": "dbt-semantic-interfaces", + "uri": "https://pypi.org/project/dbt-semantic-interfaces" + }, + { + "name": "dbt-snowflake", + "uri": "https://pypi.org/project/dbt-snowflake" + }, + { + "name": "dbt-spark", + "uri": "https://pypi.org/project/dbt-spark" + }, + { + "name": "dbus-fast", + "uri": "https://pypi.org/project/dbus-fast" + }, + { + "name": "dbutils", + "uri": "https://pypi.org/project/dbutils" + }, + { + "name": "ddsketch", + "uri": "https://pypi.org/project/ddsketch" + }, + { + "name": "ddt", + "uri": "https://pypi.org/project/ddt" + }, + { + "name": "ddtrace", + "uri": "https://pypi.org/project/ddtrace" + }, + { + "name": "debtcollector", + "uri": "https://pypi.org/project/debtcollector" + }, + { + "name": "debugpy", + "uri": "https://pypi.org/project/debugpy" + }, + { + "name": "decopatch", + "uri": "https://pypi.org/project/decopatch" + }, + { + "name": "decorator", + "uri": "https://pypi.org/project/decorator" + }, + { + "name": "deepdiff", + "uri": "https://pypi.org/project/deepdiff" + }, + { + "name": "deepmerge", + "uri": "https://pypi.org/project/deepmerge" + }, + { + "name": "defusedxml", + "uri": "https://pypi.org/project/defusedxml" + }, + { + "name": "delta", + "uri": "https://pypi.org/project/delta" + }, + { + "name": "delta-spark", + "uri": "https://pypi.org/project/delta-spark" + }, + { + "name": "deltalake", + "uri": "https://pypi.org/project/deltalake" + }, + { + "name": "dep-logic", + "uri": "https://pypi.org/project/dep-logic" + }, + { + "name": "dependency-injector", + "uri": "https://pypi.org/project/dependency-injector" + }, + { + "name": "deprecated", + "uri": "https://pypi.org/project/deprecated" + }, + { + "name": "deprecation", + "uri": "https://pypi.org/project/deprecation" + }, + { + "name": "descartes", + "uri": "https://pypi.org/project/descartes" + }, + { + "name": "detect-secrets", + "uri": "https://pypi.org/project/detect-secrets" + }, + { + "name": "dict2xml", + "uri": "https://pypi.org/project/dict2xml" + }, + { + "name": "dictdiffer", + "uri": "https://pypi.org/project/dictdiffer" + }, + { + "name": "dicttoxml", + "uri": "https://pypi.org/project/dicttoxml" + }, + { + "name": "diff-cover", + "uri": "https://pypi.org/project/diff-cover" + }, + { + "name": "diff-match-patch", + "uri": "https://pypi.org/project/diff-match-patch" + }, + { + "name": "diffusers", + "uri": "https://pypi.org/project/diffusers" + }, + { + "name": "dill", + "uri": "https://pypi.org/project/dill" + }, + { + "name": "dirtyjson", + "uri": "https://pypi.org/project/dirtyjson" + }, + { + "name": "discord", + "uri": "https://pypi.org/project/discord" + }, + { + "name": "discord-py", + "uri": "https://pypi.org/project/discord-py" + }, + { + "name": "diskcache", + "uri": "https://pypi.org/project/diskcache" + }, + { + "name": "distlib", + "uri": "https://pypi.org/project/distlib" + }, + { + "name": "distribute", + "uri": "https://pypi.org/project/distribute" + }, + { + "name": "distributed", + "uri": "https://pypi.org/project/distributed" + }, + { + "name": "distro", + "uri": "https://pypi.org/project/distro" + }, + { + "name": "dj-database-url", + "uri": "https://pypi.org/project/dj-database-url" + }, + { + "name": "django", + "uri": "https://pypi.org/project/django" + }, + { + "name": "django-allauth", + "uri": "https://pypi.org/project/django-allauth" + }, + { + "name": "django-anymail", + "uri": "https://pypi.org/project/django-anymail" + }, + { + "name": "django-appconf", + "uri": "https://pypi.org/project/django-appconf" + }, + { + "name": "django-celery-beat", + "uri": "https://pypi.org/project/django-celery-beat" + }, + { + "name": "django-celery-results", + "uri": "https://pypi.org/project/django-celery-results" + }, + { + "name": "django-compressor", + "uri": "https://pypi.org/project/django-compressor" + }, + { + "name": "django-cors-headers", + "uri": "https://pypi.org/project/django-cors-headers" + }, + { + "name": "django-countries", + "uri": "https://pypi.org/project/django-countries" + }, + { + "name": "django-crispy-forms", + "uri": "https://pypi.org/project/django-crispy-forms" + }, + { + "name": "django-csp", + "uri": "https://pypi.org/project/django-csp" + }, + { + "name": "django-debug-toolbar", + "uri": "https://pypi.org/project/django-debug-toolbar" + }, + { + "name": "django-environ", + "uri": "https://pypi.org/project/django-environ" + }, + { + "name": "django-extensions", + "uri": "https://pypi.org/project/django-extensions" + }, + { + "name": "django-filter", + "uri": "https://pypi.org/project/django-filter" + }, + { + "name": "django-health-check", + "uri": "https://pypi.org/project/django-health-check" + }, + { + "name": "django-import-export", + "uri": "https://pypi.org/project/django-import-export" + }, + { + "name": "django-ipware", + "uri": "https://pypi.org/project/django-ipware" + }, + { + "name": "django-js-asset", + "uri": "https://pypi.org/project/django-js-asset" + }, + { + "name": "django-model-utils", + "uri": "https://pypi.org/project/django-model-utils" + }, + { + "name": "django-mptt", + "uri": "https://pypi.org/project/django-mptt" + }, + { + "name": "django-oauth-toolkit", + "uri": "https://pypi.org/project/django-oauth-toolkit" + }, + { + "name": "django-otp", + "uri": "https://pypi.org/project/django-otp" + }, + { + "name": "django-phonenumber-field", + "uri": "https://pypi.org/project/django-phonenumber-field" + }, + { + "name": "django-picklefield", + "uri": "https://pypi.org/project/django-picklefield" + }, + { + "name": "django-redis", + "uri": "https://pypi.org/project/django-redis" + }, + { + "name": "django-reversion", + "uri": "https://pypi.org/project/django-reversion" + }, + { + "name": "django-ses", + "uri": "https://pypi.org/project/django-ses" + }, + { + "name": "django-silk", + "uri": "https://pypi.org/project/django-silk" + }, + { + "name": "django-simple-history", + "uri": "https://pypi.org/project/django-simple-history" + }, + { + "name": "django-storages", + "uri": "https://pypi.org/project/django-storages" + }, + { + "name": "django-stubs", + "uri": "https://pypi.org/project/django-stubs" + }, + { + "name": "django-stubs-ext", + "uri": "https://pypi.org/project/django-stubs-ext" + }, + { + "name": "django-taggit", + "uri": "https://pypi.org/project/django-taggit" + }, + { + "name": "django-timezone-field", + "uri": "https://pypi.org/project/django-timezone-field" + }, + { + "name": "django-waffle", + "uri": "https://pypi.org/project/django-waffle" + }, + { + "name": "djangorestframework", + "uri": "https://pypi.org/project/djangorestframework" + }, + { + "name": "djangorestframework-simplejwt", + "uri": "https://pypi.org/project/djangorestframework-simplejwt" + }, + { + "name": "djangorestframework-stubs", + "uri": "https://pypi.org/project/djangorestframework-stubs" + }, + { + "name": "dm-tree", + "uri": "https://pypi.org/project/dm-tree" + }, + { + "name": "dnslib", + "uri": "https://pypi.org/project/dnslib" + }, + { + "name": "dnspython", + "uri": "https://pypi.org/project/dnspython" + }, + { + "name": "docker", + "uri": "https://pypi.org/project/docker" + }, + { + "name": "docker-compose", + "uri": "https://pypi.org/project/docker-compose" + }, + { + "name": "docker-pycreds", + "uri": "https://pypi.org/project/docker-pycreds" + }, + { + "name": "dockerfile-parse", + "uri": "https://pypi.org/project/dockerfile-parse" + }, + { + "name": "dockerpty", + "uri": "https://pypi.org/project/dockerpty" + }, + { + "name": "docopt", + "uri": "https://pypi.org/project/docopt" + }, + { + "name": "docstring-parser", + "uri": "https://pypi.org/project/docstring-parser" + }, + { + "name": "documenttemplate", + "uri": "https://pypi.org/project/documenttemplate" + }, + { + "name": "docutils", + "uri": "https://pypi.org/project/docutils" + }, + { + "name": "docx2txt", + "uri": "https://pypi.org/project/docx2txt" + }, + { + "name": "dogpile-cache", + "uri": "https://pypi.org/project/dogpile-cache" + }, + { + "name": "dohq-artifactory", + "uri": "https://pypi.org/project/dohq-artifactory" + }, + { + "name": "doit", + "uri": "https://pypi.org/project/doit" + }, + { + "name": "domdf-python-tools", + "uri": "https://pypi.org/project/domdf-python-tools" + }, + { + "name": "dominate", + "uri": "https://pypi.org/project/dominate" + }, + { + "name": "dotenv", + "uri": "https://pypi.org/project/dotenv" + }, + { + "name": "dotmap", + "uri": "https://pypi.org/project/dotmap" + }, + { + "name": "dparse", + "uri": "https://pypi.org/project/dparse" + }, + { + "name": "dpath", + "uri": "https://pypi.org/project/dpath" + }, + { + "name": "dpkt", + "uri": "https://pypi.org/project/dpkt" + }, + { + "name": "drf-nested-routers", + "uri": "https://pypi.org/project/drf-nested-routers" + }, + { + "name": "drf-spectacular", + "uri": "https://pypi.org/project/drf-spectacular" + }, + { + "name": "drf-yasg", + "uri": "https://pypi.org/project/drf-yasg" + }, + { + "name": "dropbox", + "uri": "https://pypi.org/project/dropbox" + }, + { + "name": "duckdb", + "uri": "https://pypi.org/project/duckdb" + }, + { + "name": "dulwich", + "uri": "https://pypi.org/project/dulwich" + }, + { + "name": "dunamai", + "uri": "https://pypi.org/project/dunamai" + }, + { + "name": "durationpy", + "uri": "https://pypi.org/project/durationpy" + }, + { + "name": "dvclive", + "uri": "https://pypi.org/project/dvclive" + }, + { + "name": "dynaconf", + "uri": "https://pypi.org/project/dynaconf" + }, + { + "name": "dynamodb-json", + "uri": "https://pypi.org/project/dynamodb-json" + }, + { + "name": "easydict", + "uri": "https://pypi.org/project/easydict" + }, + { + "name": "easyprocess", + "uri": "https://pypi.org/project/easyprocess" + }, + { + "name": "ebcdic", + "uri": "https://pypi.org/project/ebcdic" + }, + { + "name": "ec2-metadata", + "uri": "https://pypi.org/project/ec2-metadata" + }, + { + "name": "ecdsa", + "uri": "https://pypi.org/project/ecdsa" + }, + { + "name": "ecos", + "uri": "https://pypi.org/project/ecos" + }, + { + "name": "ecs-logging", + "uri": "https://pypi.org/project/ecs-logging" + }, + { + "name": "edgegrid-python", + "uri": "https://pypi.org/project/edgegrid-python" + }, + { + "name": "editables", + "uri": "https://pypi.org/project/editables" + }, + { + "name": "editdistance", + "uri": "https://pypi.org/project/editdistance" + }, + { + "name": "editor", + "uri": "https://pypi.org/project/editor" + }, + { + "name": "editorconfig", + "uri": "https://pypi.org/project/editorconfig" + }, + { + "name": "einops", + "uri": "https://pypi.org/project/einops" + }, + { + "name": "elastic-apm", + "uri": "https://pypi.org/project/elastic-apm" + }, + { + "name": "elastic-transport", + "uri": "https://pypi.org/project/elastic-transport" + }, + { + "name": "elasticsearch", + "uri": "https://pypi.org/project/elasticsearch" + }, + { + "name": "elasticsearch-dbapi", + "uri": "https://pypi.org/project/elasticsearch-dbapi" + }, + { + "name": "elasticsearch-dsl", + "uri": "https://pypi.org/project/elasticsearch-dsl" + }, + { + "name": "elasticsearch7", + "uri": "https://pypi.org/project/elasticsearch7" + }, + { + "name": "elementary-data", + "uri": "https://pypi.org/project/elementary-data" + }, + { + "name": "elementpath", + "uri": "https://pypi.org/project/elementpath" + }, + { + "name": "elyra", + "uri": "https://pypi.org/project/elyra" + }, + { + "name": "email-validator", + "uri": "https://pypi.org/project/email-validator" + }, + { + "name": "emcee", + "uri": "https://pypi.org/project/emcee" + }, + { + "name": "emoji", + "uri": "https://pypi.org/project/emoji" + }, + { + "name": "enchant", + "uri": "https://pypi.org/project/enchant" + }, + { + "name": "enrich", + "uri": "https://pypi.org/project/enrich" + }, + { + "name": "entrypoints", + "uri": "https://pypi.org/project/entrypoints" + }, + { + "name": "enum-compat", + "uri": "https://pypi.org/project/enum-compat" + }, + { + "name": "enum34", + "uri": "https://pypi.org/project/enum34" + }, + { + "name": "envier", + "uri": "https://pypi.org/project/envier" + }, + { + "name": "environs", + "uri": "https://pypi.org/project/environs" + }, + { + "name": "envyaml", + "uri": "https://pypi.org/project/envyaml" + }, + { + "name": "ephem", + "uri": "https://pypi.org/project/ephem" + }, + { + "name": "eradicate", + "uri": "https://pypi.org/project/eradicate" + }, + { + "name": "et-xmlfile", + "uri": "https://pypi.org/project/et-xmlfile" + }, + { + "name": "eth-abi", + "uri": "https://pypi.org/project/eth-abi" + }, + { + "name": "eth-account", + "uri": "https://pypi.org/project/eth-account" + }, + { + "name": "eth-hash", + "uri": "https://pypi.org/project/eth-hash" + }, + { + "name": "eth-keyfile", + "uri": "https://pypi.org/project/eth-keyfile" + }, + { + "name": "eth-keys", + "uri": "https://pypi.org/project/eth-keys" + }, + { + "name": "eth-rlp", + "uri": "https://pypi.org/project/eth-rlp" + }, + { + "name": "eth-typing", + "uri": "https://pypi.org/project/eth-typing" + }, + { + "name": "eth-utils", + "uri": "https://pypi.org/project/eth-utils" + }, + { + "name": "etils", + "uri": "https://pypi.org/project/etils" + }, + { + "name": "eval-type-backport", + "uri": "https://pypi.org/project/eval-type-backport" + }, + { + "name": "evaluate", + "uri": "https://pypi.org/project/evaluate" + }, + { + "name": "eventlet", + "uri": "https://pypi.org/project/eventlet" + }, + { + "name": "events", + "uri": "https://pypi.org/project/events" + }, + { + "name": "evergreen-py", + "uri": "https://pypi.org/project/evergreen-py" + }, + { + "name": "evidently", + "uri": "https://pypi.org/project/evidently" + }, + { + "name": "exceptiongroup", + "uri": "https://pypi.org/project/exceptiongroup" + }, + { + "name": "exchange-calendars", + "uri": "https://pypi.org/project/exchange-calendars" + }, + { + "name": "exchangelib", + "uri": "https://pypi.org/project/exchangelib" + }, + { + "name": "execnet", + "uri": "https://pypi.org/project/execnet" + }, + { + "name": "executing", + "uri": "https://pypi.org/project/executing" + }, + { + "name": "executor", + "uri": "https://pypi.org/project/executor" + }, + { + "name": "expandvars", + "uri": "https://pypi.org/project/expandvars" + }, + { + "name": "expiringdict", + "uri": "https://pypi.org/project/expiringdict" + }, + { + "name": "extensionclass", + "uri": "https://pypi.org/project/extensionclass" + }, + { + "name": "extract-msg", + "uri": "https://pypi.org/project/extract-msg" + }, + { + "name": "extras", + "uri": "https://pypi.org/project/extras" + }, + { + "name": "eyes-common", + "uri": "https://pypi.org/project/eyes-common" + }, + { + "name": "eyes-selenium", + "uri": "https://pypi.org/project/eyes-selenium" + }, + { + "name": "f90nml", + "uri": "https://pypi.org/project/f90nml" + }, + { + "name": "fabric", + "uri": "https://pypi.org/project/fabric" + }, + { + "name": "face", + "uri": "https://pypi.org/project/face" + }, + { + "name": "facebook-business", + "uri": "https://pypi.org/project/facebook-business" + }, + { + "name": "facexlib", + "uri": "https://pypi.org/project/facexlib" + }, + { + "name": "factory-boy", + "uri": "https://pypi.org/project/factory-boy" + }, + { + "name": "fairscale", + "uri": "https://pypi.org/project/fairscale" + }, + { + "name": "faiss-cpu", + "uri": "https://pypi.org/project/faiss-cpu" + }, + { + "name": "fake-useragent", + "uri": "https://pypi.org/project/fake-useragent" + }, + { + "name": "faker", + "uri": "https://pypi.org/project/faker" + }, + { + "name": "fakeredis", + "uri": "https://pypi.org/project/fakeredis" + }, + { + "name": "falcon", + "uri": "https://pypi.org/project/falcon" + }, + { + "name": "farama-notifications", + "uri": "https://pypi.org/project/farama-notifications" + }, + { + "name": "fastapi", + "uri": "https://pypi.org/project/fastapi" + }, + { + "name": "fastapi-cli", + "uri": "https://pypi.org/project/fastapi-cli" + }, + { + "name": "fastapi-utils", + "uri": "https://pypi.org/project/fastapi-utils" + }, + { + "name": "fastavro", + "uri": "https://pypi.org/project/fastavro" + }, + { + "name": "fastcluster", + "uri": "https://pypi.org/project/fastcluster" + }, + { + "name": "fastcore", + "uri": "https://pypi.org/project/fastcore" + }, + { + "name": "fasteners", + "uri": "https://pypi.org/project/fasteners" + }, + { + "name": "faster-whisper", + "uri": "https://pypi.org/project/faster-whisper" + }, + { + "name": "fastjsonschema", + "uri": "https://pypi.org/project/fastjsonschema" + }, + { + "name": "fastparquet", + "uri": "https://pypi.org/project/fastparquet" + }, + { + "name": "fastprogress", + "uri": "https://pypi.org/project/fastprogress" + }, + { + "name": "fastrlock", + "uri": "https://pypi.org/project/fastrlock" + }, + { + "name": "fasttext", + "uri": "https://pypi.org/project/fasttext" + }, + { + "name": "fasttext-langdetect", + "uri": "https://pypi.org/project/fasttext-langdetect" + }, + { + "name": "fasttext-wheel", + "uri": "https://pypi.org/project/fasttext-wheel" + }, + { + "name": "fcm-django", + "uri": "https://pypi.org/project/fcm-django" + }, + { + "name": "feedparser", + "uri": "https://pypi.org/project/feedparser" + }, + { + "name": "ffmpeg-python", + "uri": "https://pypi.org/project/ffmpeg-python" + }, + { + "name": "ffmpy", + "uri": "https://pypi.org/project/ffmpy" + }, + { + "name": "fido2", + "uri": "https://pypi.org/project/fido2" + }, + { + "name": "filelock", + "uri": "https://pypi.org/project/filelock" + }, + { + "name": "filetype", + "uri": "https://pypi.org/project/filetype" + }, + { + "name": "filterpy", + "uri": "https://pypi.org/project/filterpy" + }, + { + "name": "find-libpython", + "uri": "https://pypi.org/project/find-libpython" + }, + { + "name": "findpython", + "uri": "https://pypi.org/project/findpython" + }, + { + "name": "findspark", + "uri": "https://pypi.org/project/findspark" + }, + { + "name": "fiona", + "uri": "https://pypi.org/project/fiona" + }, + { + "name": "fire", + "uri": "https://pypi.org/project/fire" + }, + { + "name": "firebase-admin", + "uri": "https://pypi.org/project/firebase-admin" + }, + { + "name": "fixedint", + "uri": "https://pypi.org/project/fixedint" + }, + { + "name": "fixit", + "uri": "https://pypi.org/project/fixit" + }, + { + "name": "fixtures", + "uri": "https://pypi.org/project/fixtures" + }, + { + "name": "flake8", + "uri": "https://pypi.org/project/flake8" + }, + { + "name": "flake8-black", + "uri": "https://pypi.org/project/flake8-black" + }, + { + "name": "flake8-bugbear", + "uri": "https://pypi.org/project/flake8-bugbear" + }, + { + "name": "flake8-builtins", + "uri": "https://pypi.org/project/flake8-builtins" + }, + { + "name": "flake8-comprehensions", + "uri": "https://pypi.org/project/flake8-comprehensions" + }, + { + "name": "flake8-docstrings", + "uri": "https://pypi.org/project/flake8-docstrings" + }, + { + "name": "flake8-eradicate", + "uri": "https://pypi.org/project/flake8-eradicate" + }, + { + "name": "flake8-import-order", + "uri": "https://pypi.org/project/flake8-import-order" + }, + { + "name": "flake8-isort", + "uri": "https://pypi.org/project/flake8-isort" + }, + { + "name": "flake8-polyfill", + "uri": "https://pypi.org/project/flake8-polyfill" + }, + { + "name": "flake8-print", + "uri": "https://pypi.org/project/flake8-print" + }, + { + "name": "flake8-pyproject", + "uri": "https://pypi.org/project/flake8-pyproject" + }, + { + "name": "flake8-quotes", + "uri": "https://pypi.org/project/flake8-quotes" + }, + { + "name": "flaky", + "uri": "https://pypi.org/project/flaky" + }, + { + "name": "flaml", + "uri": "https://pypi.org/project/flaml" + }, + { + "name": "flasgger", + "uri": "https://pypi.org/project/flasgger" + }, + { + "name": "flashtext", + "uri": "https://pypi.org/project/flashtext" + }, + { + "name": "flask", + "uri": "https://pypi.org/project/flask" + }, + { + "name": "flask-admin", + "uri": "https://pypi.org/project/flask-admin" + }, + { + "name": "flask-appbuilder", + "uri": "https://pypi.org/project/flask-appbuilder" + }, + { + "name": "flask-babel", + "uri": "https://pypi.org/project/flask-babel" + }, + { + "name": "flask-bcrypt", + "uri": "https://pypi.org/project/flask-bcrypt" + }, + { + "name": "flask-caching", + "uri": "https://pypi.org/project/flask-caching" + }, + { + "name": "flask-compress", + "uri": "https://pypi.org/project/flask-compress" + }, + { + "name": "flask-cors", + "uri": "https://pypi.org/project/flask-cors" + }, + { + "name": "flask-httpauth", + "uri": "https://pypi.org/project/flask-httpauth" + }, + { + "name": "flask-jwt-extended", + "uri": "https://pypi.org/project/flask-jwt-extended" + }, + { + "name": "flask-limiter", + "uri": "https://pypi.org/project/flask-limiter" + }, + { + "name": "flask-login", + "uri": "https://pypi.org/project/flask-login" + }, + { + "name": "flask-mail", + "uri": "https://pypi.org/project/flask-mail" + }, + { + "name": "flask-marshmallow", + "uri": "https://pypi.org/project/flask-marshmallow" + }, + { + "name": "flask-migrate", + "uri": "https://pypi.org/project/flask-migrate" + }, + { + "name": "flask-oidc", + "uri": "https://pypi.org/project/flask-oidc" + }, + { + "name": "flask-openid", + "uri": "https://pypi.org/project/flask-openid" + }, + { + "name": "flask-restful", + "uri": "https://pypi.org/project/flask-restful" + }, + { + "name": "flask-restx", + "uri": "https://pypi.org/project/flask-restx" + }, + { + "name": "flask-session", + "uri": "https://pypi.org/project/flask-session" + }, + { + "name": "flask-socketio", + "uri": "https://pypi.org/project/flask-socketio" + }, + { + "name": "flask-sqlalchemy", + "uri": "https://pypi.org/project/flask-sqlalchemy" + }, + { + "name": "flask-swagger-ui", + "uri": "https://pypi.org/project/flask-swagger-ui" + }, + { + "name": "flask-talisman", + "uri": "https://pypi.org/project/flask-talisman" + }, + { + "name": "flask-testing", + "uri": "https://pypi.org/project/flask-testing" + }, + { + "name": "flask-wtf", + "uri": "https://pypi.org/project/flask-wtf" + }, + { + "name": "flatbuffers", + "uri": "https://pypi.org/project/flatbuffers" + }, + { + "name": "flatdict", + "uri": "https://pypi.org/project/flatdict" + }, + { + "name": "flatten-dict", + "uri": "https://pypi.org/project/flatten-dict" + }, + { + "name": "flatten-json", + "uri": "https://pypi.org/project/flatten-json" + }, + { + "name": "flax", + "uri": "https://pypi.org/project/flax" + }, + { + "name": "flexcache", + "uri": "https://pypi.org/project/flexcache" + }, + { + "name": "flexparser", + "uri": "https://pypi.org/project/flexparser" + }, + { + "name": "flit-core", + "uri": "https://pypi.org/project/flit-core" + }, + { + "name": "flower", + "uri": "https://pypi.org/project/flower" + }, + { + "name": "fluent-logger", + "uri": "https://pypi.org/project/fluent-logger" + }, + { + "name": "folium", + "uri": "https://pypi.org/project/folium" + }, + { + "name": "fonttools", + "uri": "https://pypi.org/project/fonttools" + }, + { + "name": "formencode", + "uri": "https://pypi.org/project/formencode" + }, + { + "name": "formic2", + "uri": "https://pypi.org/project/formic2" + }, + { + "name": "formulaic", + "uri": "https://pypi.org/project/formulaic" + }, + { + "name": "fpdf", + "uri": "https://pypi.org/project/fpdf" + }, + { + "name": "fpdf2", + "uri": "https://pypi.org/project/fpdf2" + }, + { + "name": "fqdn", + "uri": "https://pypi.org/project/fqdn" + }, + { + "name": "freetype-py", + "uri": "https://pypi.org/project/freetype-py" + }, + { + "name": "freezegun", + "uri": "https://pypi.org/project/freezegun" + }, + { + "name": "frictionless", + "uri": "https://pypi.org/project/frictionless" + }, + { + "name": "frozendict", + "uri": "https://pypi.org/project/frozendict" + }, + { + "name": "frozenlist", + "uri": "https://pypi.org/project/frozenlist" + }, + { + "name": "fs", + "uri": "https://pypi.org/project/fs" + }, + { + "name": "fsspec", + "uri": "https://pypi.org/project/fsspec" + }, + { + "name": "ftfy", + "uri": "https://pypi.org/project/ftfy" + }, + { + "name": "fugue", + "uri": "https://pypi.org/project/fugue" + }, + { + "name": "func-timeout", + "uri": "https://pypi.org/project/func-timeout" + }, + { + "name": "funcsigs", + "uri": "https://pypi.org/project/funcsigs" + }, + { + "name": "functions-framework", + "uri": "https://pypi.org/project/functions-framework" + }, + { + "name": "functools32", + "uri": "https://pypi.org/project/functools32" + }, + { + "name": "funcy", + "uri": "https://pypi.org/project/funcy" + }, + { + "name": "furl", + "uri": "https://pypi.org/project/furl" + }, + { + "name": "furo", + "uri": "https://pypi.org/project/furo" + }, + { + "name": "fusepy", + "uri": "https://pypi.org/project/fusepy" + }, + { + "name": "future", + "uri": "https://pypi.org/project/future" + }, + { + "name": "future-fstrings", + "uri": "https://pypi.org/project/future-fstrings" + }, + { + "name": "futures", + "uri": "https://pypi.org/project/futures" + }, + { + "name": "fuzzywuzzy", + "uri": "https://pypi.org/project/fuzzywuzzy" + }, + { + "name": "fvcore", + "uri": "https://pypi.org/project/fvcore" + }, + { + "name": "galvani", + "uri": "https://pypi.org/project/galvani" + }, + { + "name": "gast", + "uri": "https://pypi.org/project/gast" + }, + { + "name": "gcloud-aio-auth", + "uri": "https://pypi.org/project/gcloud-aio-auth" + }, + { + "name": "gcloud-aio-bigquery", + "uri": "https://pypi.org/project/gcloud-aio-bigquery" + }, + { + "name": "gcloud-aio-storage", + "uri": "https://pypi.org/project/gcloud-aio-storage" + }, + { + "name": "gcovr", + "uri": "https://pypi.org/project/gcovr" + }, + { + "name": "gcs-oauth2-boto-plugin", + "uri": "https://pypi.org/project/gcs-oauth2-boto-plugin" + }, + { + "name": "gcsfs", + "uri": "https://pypi.org/project/gcsfs" + }, + { + "name": "gdown", + "uri": "https://pypi.org/project/gdown" + }, + { + "name": "gender-guesser", + "uri": "https://pypi.org/project/gender-guesser" + }, + { + "name": "gensim", + "uri": "https://pypi.org/project/gensim" + }, + { + "name": "genson", + "uri": "https://pypi.org/project/genson" + }, + { + "name": "geoalchemy2", + "uri": "https://pypi.org/project/geoalchemy2" + }, + { + "name": "geocoder", + "uri": "https://pypi.org/project/geocoder" + }, + { + "name": "geographiclib", + "uri": "https://pypi.org/project/geographiclib" + }, + { + "name": "geoip2", + "uri": "https://pypi.org/project/geoip2" + }, + { + "name": "geojson", + "uri": "https://pypi.org/project/geojson" + }, + { + "name": "geomet", + "uri": "https://pypi.org/project/geomet" + }, + { + "name": "geopandas", + "uri": "https://pypi.org/project/geopandas" + }, + { + "name": "geopy", + "uri": "https://pypi.org/project/geopy" + }, + { + "name": "gevent", + "uri": "https://pypi.org/project/gevent" + }, + { + "name": "gevent-websocket", + "uri": "https://pypi.org/project/gevent-websocket" + }, + { + "name": "geventhttpclient", + "uri": "https://pypi.org/project/geventhttpclient" + }, + { + "name": "ghapi", + "uri": "https://pypi.org/project/ghapi" + }, + { + "name": "ghp-import", + "uri": "https://pypi.org/project/ghp-import" + }, + { + "name": "git-remote-codecommit", + "uri": "https://pypi.org/project/git-remote-codecommit" + }, + { + "name": "gitdb", + "uri": "https://pypi.org/project/gitdb" + }, + { + "name": "gitdb2", + "uri": "https://pypi.org/project/gitdb2" + }, + { + "name": "github-heatmap", + "uri": "https://pypi.org/project/github-heatmap" + }, + { + "name": "github3-py", + "uri": "https://pypi.org/project/github3-py" + }, + { + "name": "gitpython", + "uri": "https://pypi.org/project/gitpython" + }, + { + "name": "giturlparse", + "uri": "https://pypi.org/project/giturlparse" + }, + { + "name": "glob2", + "uri": "https://pypi.org/project/glob2" + }, + { + "name": "glom", + "uri": "https://pypi.org/project/glom" + }, + { + "name": "gluonts", + "uri": "https://pypi.org/project/gluonts" + }, + { + "name": "gmpy2", + "uri": "https://pypi.org/project/gmpy2" + }, + { + "name": "gnureadline", + "uri": "https://pypi.org/project/gnureadline" + }, + { + "name": "google", + "uri": "https://pypi.org/project/google" + }, + { + "name": "google-ads", + "uri": "https://pypi.org/project/google-ads" + }, + { + "name": "google-ai-generativelanguage", + "uri": "https://pypi.org/project/google-ai-generativelanguage" + }, + { + "name": "google-analytics-admin", + "uri": "https://pypi.org/project/google-analytics-admin" + }, + { + "name": "google-analytics-data", + "uri": "https://pypi.org/project/google-analytics-data" + }, + { + "name": "google-api-core", + "uri": "https://pypi.org/project/google-api-core" + }, + { + "name": "google-api-python-client", + "uri": "https://pypi.org/project/google-api-python-client" + }, + { + "name": "google-apitools", + "uri": "https://pypi.org/project/google-apitools" + }, + { + "name": "google-auth", + "uri": "https://pypi.org/project/google-auth" + }, + { + "name": "google-auth-httplib2", + "uri": "https://pypi.org/project/google-auth-httplib2" + }, + { + "name": "google-auth-oauthlib", + "uri": "https://pypi.org/project/google-auth-oauthlib" + }, + { + "name": "google-cloud", + "uri": "https://pypi.org/project/google-cloud" + }, + { + "name": "google-cloud-access-context-manager", + "uri": "https://pypi.org/project/google-cloud-access-context-manager" + }, + { + "name": "google-cloud-aiplatform", + "uri": "https://pypi.org/project/google-cloud-aiplatform" + }, + { + "name": "google-cloud-appengine-logging", + "uri": "https://pypi.org/project/google-cloud-appengine-logging" + }, + { + "name": "google-cloud-audit-log", + "uri": "https://pypi.org/project/google-cloud-audit-log" + }, + { + "name": "google-cloud-automl", + "uri": "https://pypi.org/project/google-cloud-automl" + }, + { + "name": "google-cloud-batch", + "uri": "https://pypi.org/project/google-cloud-batch" + }, + { + "name": "google-cloud-bigquery", + "uri": "https://pypi.org/project/google-cloud-bigquery" + }, + { + "name": "google-cloud-bigquery-biglake", + "uri": "https://pypi.org/project/google-cloud-bigquery-biglake" + }, + { + "name": "google-cloud-bigquery-datatransfer", + "uri": "https://pypi.org/project/google-cloud-bigquery-datatransfer" + }, + { + "name": "google-cloud-bigquery-storage", + "uri": "https://pypi.org/project/google-cloud-bigquery-storage" + }, + { + "name": "google-cloud-bigtable", + "uri": "https://pypi.org/project/google-cloud-bigtable" + }, + { + "name": "google-cloud-build", + "uri": "https://pypi.org/project/google-cloud-build" + }, + { + "name": "google-cloud-compute", + "uri": "https://pypi.org/project/google-cloud-compute" + }, + { + "name": "google-cloud-container", + "uri": "https://pypi.org/project/google-cloud-container" + }, + { + "name": "google-cloud-core", + "uri": "https://pypi.org/project/google-cloud-core" + }, + { + "name": "google-cloud-datacatalog", + "uri": "https://pypi.org/project/google-cloud-datacatalog" + }, + { + "name": "google-cloud-dataflow-client", + "uri": "https://pypi.org/project/google-cloud-dataflow-client" + }, + { + "name": "google-cloud-dataform", + "uri": "https://pypi.org/project/google-cloud-dataform" + }, + { + "name": "google-cloud-dataplex", + "uri": "https://pypi.org/project/google-cloud-dataplex" + }, + { + "name": "google-cloud-dataproc", + "uri": "https://pypi.org/project/google-cloud-dataproc" + }, + { + "name": "google-cloud-dataproc-metastore", + "uri": "https://pypi.org/project/google-cloud-dataproc-metastore" + }, + { + "name": "google-cloud-datastore", + "uri": "https://pypi.org/project/google-cloud-datastore" + }, + { + "name": "google-cloud-discoveryengine", + "uri": "https://pypi.org/project/google-cloud-discoveryengine" + }, + { + "name": "google-cloud-dlp", + "uri": "https://pypi.org/project/google-cloud-dlp" + }, + { + "name": "google-cloud-dns", + "uri": "https://pypi.org/project/google-cloud-dns" + }, + { + "name": "google-cloud-error-reporting", + "uri": "https://pypi.org/project/google-cloud-error-reporting" + }, + { + "name": "google-cloud-firestore", + "uri": "https://pypi.org/project/google-cloud-firestore" + }, + { + "name": "google-cloud-kms", + "uri": "https://pypi.org/project/google-cloud-kms" + }, + { + "name": "google-cloud-language", + "uri": "https://pypi.org/project/google-cloud-language" + }, + { + "name": "google-cloud-logging", + "uri": "https://pypi.org/project/google-cloud-logging" + }, + { + "name": "google-cloud-memcache", + "uri": "https://pypi.org/project/google-cloud-memcache" + }, + { + "name": "google-cloud-monitoring", + "uri": "https://pypi.org/project/google-cloud-monitoring" + }, + { + "name": "google-cloud-orchestration-airflow", + "uri": "https://pypi.org/project/google-cloud-orchestration-airflow" + }, + { + "name": "google-cloud-org-policy", + "uri": "https://pypi.org/project/google-cloud-org-policy" + }, + { + "name": "google-cloud-os-config", + "uri": "https://pypi.org/project/google-cloud-os-config" + }, + { + "name": "google-cloud-os-login", + "uri": "https://pypi.org/project/google-cloud-os-login" + }, + { + "name": "google-cloud-pipeline-components", + "uri": "https://pypi.org/project/google-cloud-pipeline-components" + }, + { + "name": "google-cloud-pubsub", + "uri": "https://pypi.org/project/google-cloud-pubsub" + }, + { + "name": "google-cloud-pubsublite", + "uri": "https://pypi.org/project/google-cloud-pubsublite" + }, + { + "name": "google-cloud-recommendations-ai", + "uri": "https://pypi.org/project/google-cloud-recommendations-ai" + }, + { + "name": "google-cloud-redis", + "uri": "https://pypi.org/project/google-cloud-redis" + }, + { + "name": "google-cloud-resource-manager", + "uri": "https://pypi.org/project/google-cloud-resource-manager" + }, + { + "name": "google-cloud-run", + "uri": "https://pypi.org/project/google-cloud-run" + }, + { + "name": "google-cloud-secret-manager", + "uri": "https://pypi.org/project/google-cloud-secret-manager" + }, + { + "name": "google-cloud-spanner", + "uri": "https://pypi.org/project/google-cloud-spanner" + }, + { + "name": "google-cloud-speech", + "uri": "https://pypi.org/project/google-cloud-speech" + }, + { + "name": "google-cloud-storage", + "uri": "https://pypi.org/project/google-cloud-storage" + }, + { + "name": "google-cloud-storage-transfer", + "uri": "https://pypi.org/project/google-cloud-storage-transfer" + }, + { + "name": "google-cloud-tasks", + "uri": "https://pypi.org/project/google-cloud-tasks" + }, + { + "name": "google-cloud-texttospeech", + "uri": "https://pypi.org/project/google-cloud-texttospeech" + }, + { + "name": "google-cloud-trace", + "uri": "https://pypi.org/project/google-cloud-trace" + }, + { + "name": "google-cloud-translate", + "uri": "https://pypi.org/project/google-cloud-translate" + }, + { + "name": "google-cloud-videointelligence", + "uri": "https://pypi.org/project/google-cloud-videointelligence" + }, + { + "name": "google-cloud-vision", + "uri": "https://pypi.org/project/google-cloud-vision" + }, + { + "name": "google-cloud-workflows", + "uri": "https://pypi.org/project/google-cloud-workflows" + }, + { + "name": "google-crc32c", + "uri": "https://pypi.org/project/google-crc32c" + }, + { + "name": "google-generativeai", + "uri": "https://pypi.org/project/google-generativeai" + }, + { + "name": "google-pasta", + "uri": "https://pypi.org/project/google-pasta" + }, + { + "name": "google-re2", + "uri": "https://pypi.org/project/google-re2" + }, + { + "name": "google-reauth", + "uri": "https://pypi.org/project/google-reauth" + }, + { + "name": "google-resumable-media", + "uri": "https://pypi.org/project/google-resumable-media" + }, + { + "name": "googleapis-common-protos", + "uri": "https://pypi.org/project/googleapis-common-protos" + }, + { + "name": "googlemaps", + "uri": "https://pypi.org/project/googlemaps" + }, + { + "name": "gotrue", + "uri": "https://pypi.org/project/gotrue" + }, + { + "name": "gpiozero", + "uri": "https://pypi.org/project/gpiozero" + }, + { + "name": "gprof2dot", + "uri": "https://pypi.org/project/gprof2dot" + }, + { + "name": "gprofiler-official", + "uri": "https://pypi.org/project/gprofiler-official" + }, + { + "name": "gpustat", + "uri": "https://pypi.org/project/gpustat" + }, + { + "name": "gpxpy", + "uri": "https://pypi.org/project/gpxpy" + }, + { + "name": "gql", + "uri": "https://pypi.org/project/gql" + }, + { + "name": "gradio", + "uri": "https://pypi.org/project/gradio" + }, + { + "name": "gradio-client", + "uri": "https://pypi.org/project/gradio-client" + }, + { + "name": "grapheme", + "uri": "https://pypi.org/project/grapheme" + }, + { + "name": "graphene", + "uri": "https://pypi.org/project/graphene" + }, + { + "name": "graphframes", + "uri": "https://pypi.org/project/graphframes" + }, + { + "name": "graphlib-backport", + "uri": "https://pypi.org/project/graphlib-backport" + }, + { + "name": "graphql-core", + "uri": "https://pypi.org/project/graphql-core" + }, + { + "name": "graphql-relay", + "uri": "https://pypi.org/project/graphql-relay" + }, + { + "name": "graphviz", + "uri": "https://pypi.org/project/graphviz" + }, + { + "name": "graypy", + "uri": "https://pypi.org/project/graypy" + }, + { + "name": "great-expectations", + "uri": "https://pypi.org/project/great-expectations" + }, + { + "name": "greenlet", + "uri": "https://pypi.org/project/greenlet" + }, + { + "name": "gremlinpython", + "uri": "https://pypi.org/project/gremlinpython" + }, + { + "name": "griffe", + "uri": "https://pypi.org/project/griffe" + }, + { + "name": "grimp", + "uri": "https://pypi.org/project/grimp" + }, + { + "name": "grpc-google-iam-v1", + "uri": "https://pypi.org/project/grpc-google-iam-v1" + }, + { + "name": "grpc-interceptor", + "uri": "https://pypi.org/project/grpc-interceptor" + }, + { + "name": "grpc-stubs", + "uri": "https://pypi.org/project/grpc-stubs" + }, + { + "name": "grpcio", + "uri": "https://pypi.org/project/grpcio" + }, + { + "name": "grpcio-gcp", + "uri": "https://pypi.org/project/grpcio-gcp" + }, + { + "name": "grpcio-health-checking", + "uri": "https://pypi.org/project/grpcio-health-checking" + }, + { + "name": "grpcio-reflection", + "uri": "https://pypi.org/project/grpcio-reflection" + }, + { + "name": "grpcio-status", + "uri": "https://pypi.org/project/grpcio-status" + }, + { + "name": "grpcio-tools", + "uri": "https://pypi.org/project/grpcio-tools" + }, + { + "name": "grpclib", + "uri": "https://pypi.org/project/grpclib" + }, + { + "name": "gs-quant", + "uri": "https://pypi.org/project/gs-quant" + }, + { + "name": "gspread", + "uri": "https://pypi.org/project/gspread" + }, + { + "name": "gspread-dataframe", + "uri": "https://pypi.org/project/gspread-dataframe" + }, + { + "name": "gsutil", + "uri": "https://pypi.org/project/gsutil" + }, + { + "name": "gtts", + "uri": "https://pypi.org/project/gtts" + }, + { + "name": "gunicorn", + "uri": "https://pypi.org/project/gunicorn" + }, + { + "name": "gym", + "uri": "https://pypi.org/project/gym" + }, + { + "name": "gym-notices", + "uri": "https://pypi.org/project/gym-notices" + }, + { + "name": "gymnasium", + "uri": "https://pypi.org/project/gymnasium" + }, + { + "name": "h11", + "uri": "https://pypi.org/project/h11" + }, + { + "name": "h2", + "uri": "https://pypi.org/project/h2" + }, + { + "name": "h3", + "uri": "https://pypi.org/project/h3" + }, + { + "name": "h5netcdf", + "uri": "https://pypi.org/project/h5netcdf" + }, + { + "name": "h5py", + "uri": "https://pypi.org/project/h5py" + }, + { + "name": "halo", + "uri": "https://pypi.org/project/halo" + }, + { + "name": "hashids", + "uri": "https://pypi.org/project/hashids" + }, + { + "name": "hatch", + "uri": "https://pypi.org/project/hatch" + }, + { + "name": "hatch-fancy-pypi-readme", + "uri": "https://pypi.org/project/hatch-fancy-pypi-readme" + }, + { + "name": "hatch-requirements-txt", + "uri": "https://pypi.org/project/hatch-requirements-txt" + }, + { + "name": "hatch-vcs", + "uri": "https://pypi.org/project/hatch-vcs" + }, + { + "name": "hatchling", + "uri": "https://pypi.org/project/hatchling" + }, + { + "name": "haversine", + "uri": "https://pypi.org/project/haversine" + }, + { + "name": "hdbcli", + "uri": "https://pypi.org/project/hdbcli" + }, + { + "name": "hdfs", + "uri": "https://pypi.org/project/hdfs" + }, + { + "name": "healpy", + "uri": "https://pypi.org/project/healpy" + }, + { + "name": "hexbytes", + "uri": "https://pypi.org/project/hexbytes" + }, + { + "name": "hijri-converter", + "uri": "https://pypi.org/project/hijri-converter" + }, + { + "name": "hiredis", + "uri": "https://pypi.org/project/hiredis" + }, + { + "name": "hishel", + "uri": "https://pypi.org/project/hishel" + }, + { + "name": "hjson", + "uri": "https://pypi.org/project/hjson" + }, + { + "name": "hmmlearn", + "uri": "https://pypi.org/project/hmmlearn" + }, + { + "name": "hnswlib", + "uri": "https://pypi.org/project/hnswlib" + }, + { + "name": "holidays", + "uri": "https://pypi.org/project/holidays" + }, + { + "name": "hologram", + "uri": "https://pypi.org/project/hologram" + }, + { + "name": "honeybee-core", + "uri": "https://pypi.org/project/honeybee-core" + }, + { + "name": "honeybee-schema", + "uri": "https://pypi.org/project/honeybee-schema" + }, + { + "name": "honeybee-standards", + "uri": "https://pypi.org/project/honeybee-standards" + }, + { + "name": "hpack", + "uri": "https://pypi.org/project/hpack" + }, + { + "name": "hstspreload", + "uri": "https://pypi.org/project/hstspreload" + }, + { + "name": "html-testrunner", + "uri": "https://pypi.org/project/html-testrunner" + }, + { + "name": "html-text", + "uri": "https://pypi.org/project/html-text" + }, + { + "name": "html2text", + "uri": "https://pypi.org/project/html2text" + }, + { + "name": "html5lib", + "uri": "https://pypi.org/project/html5lib" + }, + { + "name": "htmldate", + "uri": "https://pypi.org/project/htmldate" + }, + { + "name": "htmldocx", + "uri": "https://pypi.org/project/htmldocx" + }, + { + "name": "htmlmin", + "uri": "https://pypi.org/project/htmlmin" + }, + { + "name": "httmock", + "uri": "https://pypi.org/project/httmock" + }, + { + "name": "httpcore", + "uri": "https://pypi.org/project/httpcore" + }, + { + "name": "httplib2", + "uri": "https://pypi.org/project/httplib2" + }, + { + "name": "httpretty", + "uri": "https://pypi.org/project/httpretty" + }, + { + "name": "httptools", + "uri": "https://pypi.org/project/httptools" + }, + { + "name": "httpx", + "uri": "https://pypi.org/project/httpx" + }, + { + "name": "httpx-sse", + "uri": "https://pypi.org/project/httpx-sse" + }, + { + "name": "hubspot-api-client", + "uri": "https://pypi.org/project/hubspot-api-client" + }, + { + "name": "huggingface-hub", + "uri": "https://pypi.org/project/huggingface-hub" + }, + { + "name": "humanfriendly", + "uri": "https://pypi.org/project/humanfriendly" + }, + { + "name": "humanize", + "uri": "https://pypi.org/project/humanize" + }, + { + "name": "hupper", + "uri": "https://pypi.org/project/hupper" + }, + { + "name": "hvac", + "uri": "https://pypi.org/project/hvac" + }, + { + "name": "hydra-core", + "uri": "https://pypi.org/project/hydra-core" + }, + { + "name": "hypercorn", + "uri": "https://pypi.org/project/hypercorn" + }, + { + "name": "hyperframe", + "uri": "https://pypi.org/project/hyperframe" + }, + { + "name": "hyperlink", + "uri": "https://pypi.org/project/hyperlink" + }, + { + "name": "hyperopt", + "uri": "https://pypi.org/project/hyperopt" + }, + { + "name": "hyperpyyaml", + "uri": "https://pypi.org/project/hyperpyyaml" + }, + { + "name": "hypothesis", + "uri": "https://pypi.org/project/hypothesis" + }, + { + "name": "ibm-cloud-sdk-core", + "uri": "https://pypi.org/project/ibm-cloud-sdk-core" + }, + { + "name": "ibm-db", + "uri": "https://pypi.org/project/ibm-db" + }, + { + "name": "ibm-platform-services", + "uri": "https://pypi.org/project/ibm-platform-services" + }, + { + "name": "icalendar", + "uri": "https://pypi.org/project/icalendar" + }, + { + "name": "icdiff", + "uri": "https://pypi.org/project/icdiff" + }, + { + "name": "icecream", + "uri": "https://pypi.org/project/icecream" + }, + { + "name": "identify", + "uri": "https://pypi.org/project/identify" + }, + { + "name": "idna", + "uri": "https://pypi.org/project/idna" + }, + { + "name": "idna-ssl", + "uri": "https://pypi.org/project/idna-ssl" + }, + { + "name": "ifaddr", + "uri": "https://pypi.org/project/ifaddr" + }, + { + "name": "igraph", + "uri": "https://pypi.org/project/igraph" + }, + { + "name": "ijson", + "uri": "https://pypi.org/project/ijson" + }, + { + "name": "imagecodecs", + "uri": "https://pypi.org/project/imagecodecs" + }, + { + "name": "imagehash", + "uri": "https://pypi.org/project/imagehash" + }, + { + "name": "imageio", + "uri": "https://pypi.org/project/imageio" + }, + { + "name": "imageio-ffmpeg", + "uri": "https://pypi.org/project/imageio-ffmpeg" + }, + { + "name": "imagesize", + "uri": "https://pypi.org/project/imagesize" + }, + { + "name": "imapclient", + "uri": "https://pypi.org/project/imapclient" + }, + { + "name": "imath", + "uri": "https://pypi.org/project/imath" + }, + { + "name": "imbalanced-learn", + "uri": "https://pypi.org/project/imbalanced-learn" + }, + { + "name": "imblearn", + "uri": "https://pypi.org/project/imblearn" + }, + { + "name": "imdbpy", + "uri": "https://pypi.org/project/imdbpy" + }, + { + "name": "immutabledict", + "uri": "https://pypi.org/project/immutabledict" + }, + { + "name": "immutables", + "uri": "https://pypi.org/project/immutables" + }, + { + "name": "import-linter", + "uri": "https://pypi.org/project/import-linter" + }, + { + "name": "importlib", + "uri": "https://pypi.org/project/importlib" + }, + { + "name": "importlib-metadata", + "uri": "https://pypi.org/project/importlib-metadata" + }, + { + "name": "importlib-resources", + "uri": "https://pypi.org/project/importlib-resources" + }, + { + "name": "impyla", + "uri": "https://pypi.org/project/impyla" + }, + { + "name": "imutils", + "uri": "https://pypi.org/project/imutils" + }, + { + "name": "incremental", + "uri": "https://pypi.org/project/incremental" + }, + { + "name": "inexactsearch", + "uri": "https://pypi.org/project/inexactsearch" + }, + { + "name": "inflate64", + "uri": "https://pypi.org/project/inflate64" + }, + { + "name": "inflect", + "uri": "https://pypi.org/project/inflect" + }, + { + "name": "inflection", + "uri": "https://pypi.org/project/inflection" + }, + { + "name": "influxdb", + "uri": "https://pypi.org/project/influxdb" + }, + { + "name": "influxdb-client", + "uri": "https://pypi.org/project/influxdb-client" + }, + { + "name": "iniconfig", + "uri": "https://pypi.org/project/iniconfig" + }, + { + "name": "inject", + "uri": "https://pypi.org/project/inject" + }, + { + "name": "injector", + "uri": "https://pypi.org/project/injector" + }, + { + "name": "inquirer", + "uri": "https://pypi.org/project/inquirer" + }, + { + "name": "inquirerpy", + "uri": "https://pypi.org/project/inquirerpy" + }, + { + "name": "insight-cli", + "uri": "https://pypi.org/project/insight-cli" + }, + { + "name": "install-jdk", + "uri": "https://pypi.org/project/install-jdk" + }, + { + "name": "installer", + "uri": "https://pypi.org/project/installer" + }, + { + "name": "intelhex", + "uri": "https://pypi.org/project/intelhex" + }, + { + "name": "interegular", + "uri": "https://pypi.org/project/interegular" + }, + { + "name": "interface-meta", + "uri": "https://pypi.org/project/interface-meta" + }, + { + "name": "intervaltree", + "uri": "https://pypi.org/project/intervaltree" + }, + { + "name": "invoke", + "uri": "https://pypi.org/project/invoke" + }, + { + "name": "iopath", + "uri": "https://pypi.org/project/iopath" + }, + { + "name": "ipaddress", + "uri": "https://pypi.org/project/ipaddress" + }, + { + "name": "ipdb", + "uri": "https://pypi.org/project/ipdb" + }, + { + "name": "ipykernel", + "uri": "https://pypi.org/project/ipykernel" + }, + { + "name": "ipython", + "uri": "https://pypi.org/project/ipython" + }, + { + "name": "ipython-genutils", + "uri": "https://pypi.org/project/ipython-genutils" + }, + { + "name": "ipywidgets", + "uri": "https://pypi.org/project/ipywidgets" + }, + { + "name": "isbnlib", + "uri": "https://pypi.org/project/isbnlib" + }, + { + "name": "iso3166", + "uri": "https://pypi.org/project/iso3166" + }, + { + "name": "iso8601", + "uri": "https://pypi.org/project/iso8601" + }, + { + "name": "isodate", + "uri": "https://pypi.org/project/isodate" + }, + { + "name": "isoduration", + "uri": "https://pypi.org/project/isoduration" + }, + { + "name": "isort", + "uri": "https://pypi.org/project/isort" + }, + { + "name": "isoweek", + "uri": "https://pypi.org/project/isoweek" + }, + { + "name": "itemadapter", + "uri": "https://pypi.org/project/itemadapter" + }, + { + "name": "itemloaders", + "uri": "https://pypi.org/project/itemloaders" + }, + { + "name": "iterative-telemetry", + "uri": "https://pypi.org/project/iterative-telemetry" + }, + { + "name": "itsdangerous", + "uri": "https://pypi.org/project/itsdangerous" + }, + { + "name": "itypes", + "uri": "https://pypi.org/project/itypes" + }, + { + "name": "j2cli", + "uri": "https://pypi.org/project/j2cli" + }, + { + "name": "jaconv", + "uri": "https://pypi.org/project/jaconv" + }, + { + "name": "janus", + "uri": "https://pypi.org/project/janus" + }, + { + "name": "jaraco-classes", + "uri": "https://pypi.org/project/jaraco-classes" + }, + { + "name": "jaraco-collections", + "uri": "https://pypi.org/project/jaraco-collections" + }, + { + "name": "jaraco-context", + "uri": "https://pypi.org/project/jaraco-context" + }, + { + "name": "jaraco-functools", + "uri": "https://pypi.org/project/jaraco-functools" + }, + { + "name": "jaraco-text", + "uri": "https://pypi.org/project/jaraco-text" + }, + { + "name": "java-access-bridge-wrapper", + "uri": "https://pypi.org/project/java-access-bridge-wrapper" + }, + { + "name": "java-manifest", + "uri": "https://pypi.org/project/java-manifest" + }, + { + "name": "javaproperties", + "uri": "https://pypi.org/project/javaproperties" + }, + { + "name": "jax", + "uri": "https://pypi.org/project/jax" + }, + { + "name": "jaxlib", + "uri": "https://pypi.org/project/jaxlib" + }, + { + "name": "jaxtyping", + "uri": "https://pypi.org/project/jaxtyping" + }, + { + "name": "jaydebeapi", + "uri": "https://pypi.org/project/jaydebeapi" + }, + { + "name": "jdcal", + "uri": "https://pypi.org/project/jdcal" + }, + { + "name": "jedi", + "uri": "https://pypi.org/project/jedi" + }, + { + "name": "jeepney", + "uri": "https://pypi.org/project/jeepney" + }, + { + "name": "jellyfish", + "uri": "https://pypi.org/project/jellyfish" + }, + { + "name": "jenkinsapi", + "uri": "https://pypi.org/project/jenkinsapi" + }, + { + "name": "jetblack-iso8601", + "uri": "https://pypi.org/project/jetblack-iso8601" + }, + { + "name": "jieba", + "uri": "https://pypi.org/project/jieba" + }, + { + "name": "jinja2", + "uri": "https://pypi.org/project/jinja2" + }, + { + "name": "jinja2-humanize-extension", + "uri": "https://pypi.org/project/jinja2-humanize-extension" + }, + { + "name": "jinja2-pluralize", + "uri": "https://pypi.org/project/jinja2-pluralize" + }, + { + "name": "jinja2-simple-tags", + "uri": "https://pypi.org/project/jinja2-simple-tags" + }, + { + "name": "jinja2-time", + "uri": "https://pypi.org/project/jinja2-time" + }, + { + "name": "jinjasql", + "uri": "https://pypi.org/project/jinjasql" + }, + { + "name": "jira", + "uri": "https://pypi.org/project/jira" + }, + { + "name": "jiter", + "uri": "https://pypi.org/project/jiter" + }, + { + "name": "jiwer", + "uri": "https://pypi.org/project/jiwer" + }, + { + "name": "jmespath", + "uri": "https://pypi.org/project/jmespath" + }, + { + "name": "joblib", + "uri": "https://pypi.org/project/joblib" + }, + { + "name": "josepy", + "uri": "https://pypi.org/project/josepy" + }, + { + "name": "joserfc", + "uri": "https://pypi.org/project/joserfc" + }, + { + "name": "jplephem", + "uri": "https://pypi.org/project/jplephem" + }, + { + "name": "jproperties", + "uri": "https://pypi.org/project/jproperties" + }, + { + "name": "jpype1", + "uri": "https://pypi.org/project/jpype1" + }, + { + "name": "jq", + "uri": "https://pypi.org/project/jq" + }, + { + "name": "js2py", + "uri": "https://pypi.org/project/js2py" + }, + { + "name": "jsbeautifier", + "uri": "https://pypi.org/project/jsbeautifier" + }, + { + "name": "jschema-to-python", + "uri": "https://pypi.org/project/jschema-to-python" + }, + { + "name": "jsii", + "uri": "https://pypi.org/project/jsii" + }, + { + "name": "jsmin", + "uri": "https://pypi.org/project/jsmin" + }, + { + "name": "json-delta", + "uri": "https://pypi.org/project/json-delta" + }, + { + "name": "json-log-formatter", + "uri": "https://pypi.org/project/json-log-formatter" + }, + { + "name": "json-logging", + "uri": "https://pypi.org/project/json-logging" + }, + { + "name": "json-merge-patch", + "uri": "https://pypi.org/project/json-merge-patch" + }, + { + "name": "json2html", + "uri": "https://pypi.org/project/json2html" + }, + { + "name": "json5", + "uri": "https://pypi.org/project/json5" + }, + { + "name": "jsonargparse", + "uri": "https://pypi.org/project/jsonargparse" + }, + { + "name": "jsonconversion", + "uri": "https://pypi.org/project/jsonconversion" + }, + { + "name": "jsondiff", + "uri": "https://pypi.org/project/jsondiff" + }, + { + "name": "jsonlines", + "uri": "https://pypi.org/project/jsonlines" + }, + { + "name": "jsonmerge", + "uri": "https://pypi.org/project/jsonmerge" + }, + { + "name": "jsonpatch", + "uri": "https://pypi.org/project/jsonpatch" + }, + { + "name": "jsonpath-ng", + "uri": "https://pypi.org/project/jsonpath-ng" + }, + { + "name": "jsonpath-python", + "uri": "https://pypi.org/project/jsonpath-python" + }, + { + "name": "jsonpath-rw", + "uri": "https://pypi.org/project/jsonpath-rw" + }, + { + "name": "jsonpickle", + "uri": "https://pypi.org/project/jsonpickle" + }, + { + "name": "jsonpointer", + "uri": "https://pypi.org/project/jsonpointer" + }, + { + "name": "jsonref", + "uri": "https://pypi.org/project/jsonref" + }, + { + "name": "jsons", + "uri": "https://pypi.org/project/jsons" + }, + { + "name": "jsonschema", + "uri": "https://pypi.org/project/jsonschema" + }, + { + "name": "jsonschema-path", + "uri": "https://pypi.org/project/jsonschema-path" + }, + { + "name": "jsonschema-spec", + "uri": "https://pypi.org/project/jsonschema-spec" + }, + { + "name": "jsonschema-specifications", + "uri": "https://pypi.org/project/jsonschema-specifications" + }, + { + "name": "jstyleson", + "uri": "https://pypi.org/project/jstyleson" + }, + { + "name": "junit-xml", + "uri": "https://pypi.org/project/junit-xml" + }, + { + "name": "junitparser", + "uri": "https://pypi.org/project/junitparser" + }, + { + "name": "jupyter", + "uri": "https://pypi.org/project/jupyter" + }, + { + "name": "jupyter-client", + "uri": "https://pypi.org/project/jupyter-client" + }, + { + "name": "jupyter-console", + "uri": "https://pypi.org/project/jupyter-console" + }, + { + "name": "jupyter-core", + "uri": "https://pypi.org/project/jupyter-core" + }, + { + "name": "jupyter-events", + "uri": "https://pypi.org/project/jupyter-events" + }, + { + "name": "jupyter-lsp", + "uri": "https://pypi.org/project/jupyter-lsp" + }, + { + "name": "jupyter-packaging", + "uri": "https://pypi.org/project/jupyter-packaging" + }, + { + "name": "jupyter-server", + "uri": "https://pypi.org/project/jupyter-server" + }, + { + "name": "jupyter-server-fileid", + "uri": "https://pypi.org/project/jupyter-server-fileid" + }, + { + "name": "jupyter-server-terminals", + "uri": "https://pypi.org/project/jupyter-server-terminals" + }, + { + "name": "jupyter-server-ydoc", + "uri": "https://pypi.org/project/jupyter-server-ydoc" + }, + { + "name": "jupyter-ydoc", + "uri": "https://pypi.org/project/jupyter-ydoc" + }, + { + "name": "jupyterlab", + "uri": "https://pypi.org/project/jupyterlab" + }, + { + "name": "jupyterlab-pygments", + "uri": "https://pypi.org/project/jupyterlab-pygments" + }, + { + "name": "jupyterlab-server", + "uri": "https://pypi.org/project/jupyterlab-server" + }, + { + "name": "jupyterlab-widgets", + "uri": "https://pypi.org/project/jupyterlab-widgets" + }, + { + "name": "jupytext", + "uri": "https://pypi.org/project/jupytext" + }, + { + "name": "justext", + "uri": "https://pypi.org/project/justext" + }, + { + "name": "jwcrypto", + "uri": "https://pypi.org/project/jwcrypto" + }, + { + "name": "jwt", + "uri": "https://pypi.org/project/jwt" + }, + { + "name": "kafka-python", + "uri": "https://pypi.org/project/kafka-python" + }, + { + "name": "kaitaistruct", + "uri": "https://pypi.org/project/kaitaistruct" + }, + { + "name": "kaleido", + "uri": "https://pypi.org/project/kaleido" + }, + { + "name": "kazoo", + "uri": "https://pypi.org/project/kazoo" + }, + { + "name": "kconfiglib", + "uri": "https://pypi.org/project/kconfiglib" + }, + { + "name": "keras", + "uri": "https://pypi.org/project/keras" + }, + { + "name": "keras-applications", + "uri": "https://pypi.org/project/keras-applications" + }, + { + "name": "keras-preprocessing", + "uri": "https://pypi.org/project/keras-preprocessing" + }, + { + "name": "keyring", + "uri": "https://pypi.org/project/keyring" + }, + { + "name": "keyrings-alt", + "uri": "https://pypi.org/project/keyrings-alt" + }, + { + "name": "keyrings-google-artifactregistry-auth", + "uri": "https://pypi.org/project/keyrings-google-artifactregistry-auth" + }, + { + "name": "keystoneauth1", + "uri": "https://pypi.org/project/keystoneauth1" + }, + { + "name": "kfp", + "uri": "https://pypi.org/project/kfp" + }, + { + "name": "kfp-pipeline-spec", + "uri": "https://pypi.org/project/kfp-pipeline-spec" + }, + { + "name": "kfp-server-api", + "uri": "https://pypi.org/project/kfp-server-api" + }, + { + "name": "kivy", + "uri": "https://pypi.org/project/kivy" + }, + { + "name": "kiwisolver", + "uri": "https://pypi.org/project/kiwisolver" + }, + { + "name": "knack", + "uri": "https://pypi.org/project/knack" + }, + { + "name": "koalas", + "uri": "https://pypi.org/project/koalas" + }, + { + "name": "kombu", + "uri": "https://pypi.org/project/kombu" + }, + { + "name": "korean-lunar-calendar", + "uri": "https://pypi.org/project/korean-lunar-calendar" + }, + { + "name": "kornia", + "uri": "https://pypi.org/project/kornia" + }, + { + "name": "kornia-rs", + "uri": "https://pypi.org/project/kornia-rs" + }, + { + "name": "kubernetes", + "uri": "https://pypi.org/project/kubernetes" + }, + { + "name": "kubernetes-asyncio", + "uri": "https://pypi.org/project/kubernetes-asyncio" + }, + { + "name": "ladybug-core", + "uri": "https://pypi.org/project/ladybug-core" + }, + { + "name": "ladybug-display", + "uri": "https://pypi.org/project/ladybug-display" + }, + { + "name": "ladybug-geometry", + "uri": "https://pypi.org/project/ladybug-geometry" + }, + { + "name": "ladybug-geometry-polyskel", + "uri": "https://pypi.org/project/ladybug-geometry-polyskel" + }, + { + "name": "lagom", + "uri": "https://pypi.org/project/lagom" + }, + { + "name": "langchain", + "uri": "https://pypi.org/project/langchain" + }, + { + "name": "langchain-anthropic", + "uri": "https://pypi.org/project/langchain-anthropic" + }, + { + "name": "langchain-aws", + "uri": "https://pypi.org/project/langchain-aws" + }, + { + "name": "langchain-community", + "uri": "https://pypi.org/project/langchain-community" + }, + { + "name": "langchain-core", + "uri": "https://pypi.org/project/langchain-core" + }, + { + "name": "langchain-experimental", + "uri": "https://pypi.org/project/langchain-experimental" + }, + { + "name": "langchain-google-vertexai", + "uri": "https://pypi.org/project/langchain-google-vertexai" + }, + { + "name": "langchain-openai", + "uri": "https://pypi.org/project/langchain-openai" + }, + { + "name": "langchain-text-splitters", + "uri": "https://pypi.org/project/langchain-text-splitters" + }, + { + "name": "langcodes", + "uri": "https://pypi.org/project/langcodes" + }, + { + "name": "langdetect", + "uri": "https://pypi.org/project/langdetect" + }, + { + "name": "langgraph", + "uri": "https://pypi.org/project/langgraph" + }, + { + "name": "langsmith", + "uri": "https://pypi.org/project/langsmith" + }, + { + "name": "language-data", + "uri": "https://pypi.org/project/language-data" + }, + { + "name": "language-tags", + "uri": "https://pypi.org/project/language-tags" + }, + { + "name": "language-tool-python", + "uri": "https://pypi.org/project/language-tool-python" + }, + { + "name": "lark", + "uri": "https://pypi.org/project/lark" + }, + { + "name": "lark-parser", + "uri": "https://pypi.org/project/lark-parser" + }, + { + "name": "lasio", + "uri": "https://pypi.org/project/lasio" + }, + { + "name": "latexcodec", + "uri": "https://pypi.org/project/latexcodec" + }, + { + "name": "launchdarkly-server-sdk", + "uri": "https://pypi.org/project/launchdarkly-server-sdk" + }, + { + "name": "lazy-loader", + "uri": "https://pypi.org/project/lazy-loader" + }, + { + "name": "lazy-object-proxy", + "uri": "https://pypi.org/project/lazy-object-proxy" + }, + { + "name": "ldap3", + "uri": "https://pypi.org/project/ldap3" + }, + { + "name": "leather", + "uri": "https://pypi.org/project/leather" + }, + { + "name": "levenshtein", + "uri": "https://pypi.org/project/levenshtein" + }, + { + "name": "libclang", + "uri": "https://pypi.org/project/libclang" + }, + { + "name": "libcst", + "uri": "https://pypi.org/project/libcst" + }, + { + "name": "libretranslatepy", + "uri": "https://pypi.org/project/libretranslatepy" + }, + { + "name": "librosa", + "uri": "https://pypi.org/project/librosa" + }, + { + "name": "libsass", + "uri": "https://pypi.org/project/libsass" + }, + { + "name": "license-expression", + "uri": "https://pypi.org/project/license-expression" + }, + { + "name": "lifelines", + "uri": "https://pypi.org/project/lifelines" + }, + { + "name": "lightgbm", + "uri": "https://pypi.org/project/lightgbm" + }, + { + "name": "lightning", + "uri": "https://pypi.org/project/lightning" + }, + { + "name": "lightning-utilities", + "uri": "https://pypi.org/project/lightning-utilities" + }, + { + "name": "limits", + "uri": "https://pypi.org/project/limits" + }, + { + "name": "line-profiler", + "uri": "https://pypi.org/project/line-profiler" + }, + { + "name": "linecache2", + "uri": "https://pypi.org/project/linecache2" + }, + { + "name": "linkify-it-py", + "uri": "https://pypi.org/project/linkify-it-py" + }, + { + "name": "lit", + "uri": "https://pypi.org/project/lit" + }, + { + "name": "litellm", + "uri": "https://pypi.org/project/litellm" + }, + { + "name": "livereload", + "uri": "https://pypi.org/project/livereload" + }, + { + "name": "livy", + "uri": "https://pypi.org/project/livy" + }, + { + "name": "lizard", + "uri": "https://pypi.org/project/lizard" + }, + { + "name": "llama-cloud", + "uri": "https://pypi.org/project/llama-cloud" + }, + { + "name": "llama-index", + "uri": "https://pypi.org/project/llama-index" + }, + { + "name": "llama-index-agent-openai", + "uri": "https://pypi.org/project/llama-index-agent-openai" + }, + { + "name": "llama-index-cli", + "uri": "https://pypi.org/project/llama-index-cli" + }, + { + "name": "llama-index-core", + "uri": "https://pypi.org/project/llama-index-core" + }, + { + "name": "llama-index-embeddings-openai", + "uri": "https://pypi.org/project/llama-index-embeddings-openai" + }, + { + "name": "llama-index-indices-managed-llama-cloud", + "uri": "https://pypi.org/project/llama-index-indices-managed-llama-cloud" + }, + { + "name": "llama-index-legacy", + "uri": "https://pypi.org/project/llama-index-legacy" + }, + { + "name": "llama-index-llms-openai", + "uri": "https://pypi.org/project/llama-index-llms-openai" + }, + { + "name": "llama-index-multi-modal-llms-openai", + "uri": "https://pypi.org/project/llama-index-multi-modal-llms-openai" + }, + { + "name": "llama-index-program-openai", + "uri": "https://pypi.org/project/llama-index-program-openai" + }, + { + "name": "llama-index-question-gen-openai", + "uri": "https://pypi.org/project/llama-index-question-gen-openai" + }, + { + "name": "llama-index-readers-file", + "uri": "https://pypi.org/project/llama-index-readers-file" + }, + { + "name": "llama-index-readers-llama-parse", + "uri": "https://pypi.org/project/llama-index-readers-llama-parse" + }, + { + "name": "llama-parse", + "uri": "https://pypi.org/project/llama-parse" + }, + { + "name": "llvmlite", + "uri": "https://pypi.org/project/llvmlite" + }, + { + "name": "lm-format-enforcer", + "uri": "https://pypi.org/project/lm-format-enforcer" + }, + { + "name": "lmdb", + "uri": "https://pypi.org/project/lmdb" + }, + { + "name": "lmfit", + "uri": "https://pypi.org/project/lmfit" + }, + { + "name": "locket", + "uri": "https://pypi.org/project/locket" + }, + { + "name": "lockfile", + "uri": "https://pypi.org/project/lockfile" + }, + { + "name": "locust", + "uri": "https://pypi.org/project/locust" + }, + { + "name": "log-symbols", + "uri": "https://pypi.org/project/log-symbols" + }, + { + "name": "logbook", + "uri": "https://pypi.org/project/logbook" + }, + { + "name": "logging-azure-rest", + "uri": "https://pypi.org/project/logging-azure-rest" + }, + { + "name": "loguru", + "uri": "https://pypi.org/project/loguru" + }, + { + "name": "logz", + "uri": "https://pypi.org/project/logz" + }, + { + "name": "logzero", + "uri": "https://pypi.org/project/logzero" + }, + { + "name": "looker-sdk", + "uri": "https://pypi.org/project/looker-sdk" + }, + { + "name": "looseversion", + "uri": "https://pypi.org/project/looseversion" + }, + { + "name": "lpips", + "uri": "https://pypi.org/project/lpips" + }, + { + "name": "lru-dict", + "uri": "https://pypi.org/project/lru-dict" + }, + { + "name": "lunarcalendar", + "uri": "https://pypi.org/project/lunarcalendar" + }, + { + "name": "lunardate", + "uri": "https://pypi.org/project/lunardate" + }, + { + "name": "lxml", + "uri": "https://pypi.org/project/lxml" + }, + { + "name": "lxml-html-clean", + "uri": "https://pypi.org/project/lxml-html-clean" + }, + { + "name": "lz4", + "uri": "https://pypi.org/project/lz4" + }, + { + "name": "macholib", + "uri": "https://pypi.org/project/macholib" + }, + { + "name": "magicattr", + "uri": "https://pypi.org/project/magicattr" + }, + { + "name": "makefun", + "uri": "https://pypi.org/project/makefun" + }, + { + "name": "mako", + "uri": "https://pypi.org/project/mako" + }, + { + "name": "mammoth", + "uri": "https://pypi.org/project/mammoth" + }, + { + "name": "mando", + "uri": "https://pypi.org/project/mando" + }, + { + "name": "mangum", + "uri": "https://pypi.org/project/mangum" + }, + { + "name": "mapbox-earcut", + "uri": "https://pypi.org/project/mapbox-earcut" + }, + { + "name": "marisa-trie", + "uri": "https://pypi.org/project/marisa-trie" + }, + { + "name": "markdown", + "uri": "https://pypi.org/project/markdown" + }, + { + "name": "markdown-it-py", + "uri": "https://pypi.org/project/markdown-it-py" + }, + { + "name": "markdown2", + "uri": "https://pypi.org/project/markdown2" + }, + { + "name": "markdownify", + "uri": "https://pypi.org/project/markdownify" + }, + { + "name": "markupsafe", + "uri": "https://pypi.org/project/markupsafe" + }, + { + "name": "marshmallow", + "uri": "https://pypi.org/project/marshmallow" + }, + { + "name": "marshmallow-dataclass", + "uri": "https://pypi.org/project/marshmallow-dataclass" + }, + { + "name": "marshmallow-enum", + "uri": "https://pypi.org/project/marshmallow-enum" + }, + { + "name": "marshmallow-oneofschema", + "uri": "https://pypi.org/project/marshmallow-oneofschema" + }, + { + "name": "marshmallow-sqlalchemy", + "uri": "https://pypi.org/project/marshmallow-sqlalchemy" + }, + { + "name": "mashumaro", + "uri": "https://pypi.org/project/mashumaro" + }, + { + "name": "matplotlib", + "uri": "https://pypi.org/project/matplotlib" + }, + { + "name": "matplotlib-inline", + "uri": "https://pypi.org/project/matplotlib-inline" + }, + { + "name": "maturin", + "uri": "https://pypi.org/project/maturin" + }, + { + "name": "maxminddb", + "uri": "https://pypi.org/project/maxminddb" + }, + { + "name": "mbstrdecoder", + "uri": "https://pypi.org/project/mbstrdecoder" + }, + { + "name": "mccabe", + "uri": "https://pypi.org/project/mccabe" + }, + { + "name": "mchammer", + "uri": "https://pypi.org/project/mchammer" + }, + { + "name": "mdit-py-plugins", + "uri": "https://pypi.org/project/mdit-py-plugins" + }, + { + "name": "mdurl", + "uri": "https://pypi.org/project/mdurl" + }, + { + "name": "mdx-truly-sane-lists", + "uri": "https://pypi.org/project/mdx-truly-sane-lists" + }, + { + "name": "mecab-python3", + "uri": "https://pypi.org/project/mecab-python3" + }, + { + "name": "mediapipe", + "uri": "https://pypi.org/project/mediapipe" + }, + { + "name": "megatron-core", + "uri": "https://pypi.org/project/megatron-core" + }, + { + "name": "memoization", + "uri": "https://pypi.org/project/memoization" + }, + { + "name": "memory-profiler", + "uri": "https://pypi.org/project/memory-profiler" + }, + { + "name": "memray", + "uri": "https://pypi.org/project/memray" + }, + { + "name": "mercantile", + "uri": "https://pypi.org/project/mercantile" + }, + { + "name": "mergedeep", + "uri": "https://pypi.org/project/mergedeep" + }, + { + "name": "meson", + "uri": "https://pypi.org/project/meson" + }, + { + "name": "meson-python", + "uri": "https://pypi.org/project/meson-python" + }, + { + "name": "methodtools", + "uri": "https://pypi.org/project/methodtools" + }, + { + "name": "mf2py", + "uri": "https://pypi.org/project/mf2py" + }, + { + "name": "microsoft-kiota-abstractions", + "uri": "https://pypi.org/project/microsoft-kiota-abstractions" + }, + { + "name": "microsoft-kiota-authentication-azure", + "uri": "https://pypi.org/project/microsoft-kiota-authentication-azure" + }, + { + "name": "microsoft-kiota-http", + "uri": "https://pypi.org/project/microsoft-kiota-http" + }, + { + "name": "microsoft-kiota-serialization-json", + "uri": "https://pypi.org/project/microsoft-kiota-serialization-json" + }, + { + "name": "microsoft-kiota-serialization-text", + "uri": "https://pypi.org/project/microsoft-kiota-serialization-text" + }, + { + "name": "mimesis", + "uri": "https://pypi.org/project/mimesis" + }, + { + "name": "minidump", + "uri": "https://pypi.org/project/minidump" + }, + { + "name": "minimal-snowplow-tracker", + "uri": "https://pypi.org/project/minimal-snowplow-tracker" + }, + { + "name": "minio", + "uri": "https://pypi.org/project/minio" + }, + { + "name": "mistune", + "uri": "https://pypi.org/project/mistune" + }, + { + "name": "mixpanel", + "uri": "https://pypi.org/project/mixpanel" + }, + { + "name": "mizani", + "uri": "https://pypi.org/project/mizani" + }, + { + "name": "mkdocs", + "uri": "https://pypi.org/project/mkdocs" + }, + { + "name": "mkdocs-autorefs", + "uri": "https://pypi.org/project/mkdocs-autorefs" + }, + { + "name": "mkdocs-get-deps", + "uri": "https://pypi.org/project/mkdocs-get-deps" + }, + { + "name": "mkdocs-material", + "uri": "https://pypi.org/project/mkdocs-material" + }, + { + "name": "mkdocs-material-extensions", + "uri": "https://pypi.org/project/mkdocs-material-extensions" + }, + { + "name": "mkdocstrings", + "uri": "https://pypi.org/project/mkdocstrings" + }, + { + "name": "mkdocstrings-python", + "uri": "https://pypi.org/project/mkdocstrings-python" + }, + { + "name": "ml-dtypes", + "uri": "https://pypi.org/project/ml-dtypes" + }, + { + "name": "mleap", + "uri": "https://pypi.org/project/mleap" + }, + { + "name": "mlflow", + "uri": "https://pypi.org/project/mlflow" + }, + { + "name": "mlflow-skinny", + "uri": "https://pypi.org/project/mlflow-skinny" + }, + { + "name": "mlserver", + "uri": "https://pypi.org/project/mlserver" + }, + { + "name": "mltable", + "uri": "https://pypi.org/project/mltable" + }, + { + "name": "mlxtend", + "uri": "https://pypi.org/project/mlxtend" + }, + { + "name": "mmcif", + "uri": "https://pypi.org/project/mmcif" + }, + { + "name": "mmcif-pdbx", + "uri": "https://pypi.org/project/mmcif-pdbx" + }, + { + "name": "mmh3", + "uri": "https://pypi.org/project/mmh3" + }, + { + "name": "mock", + "uri": "https://pypi.org/project/mock" + }, + { + "name": "mockito", + "uri": "https://pypi.org/project/mockito" + }, + { + "name": "model-bakery", + "uri": "https://pypi.org/project/model-bakery" + }, + { + "name": "modin", + "uri": "https://pypi.org/project/modin" + }, + { + "name": "molecule", + "uri": "https://pypi.org/project/molecule" + }, + { + "name": "mongoengine", + "uri": "https://pypi.org/project/mongoengine" + }, + { + "name": "mongomock", + "uri": "https://pypi.org/project/mongomock" + }, + { + "name": "monotonic", + "uri": "https://pypi.org/project/monotonic" + }, + { + "name": "more-itertools", + "uri": "https://pypi.org/project/more-itertools" + }, + { + "name": "moreorless", + "uri": "https://pypi.org/project/moreorless" + }, + { + "name": "morse3", + "uri": "https://pypi.org/project/morse3" + }, + { + "name": "moto", + "uri": "https://pypi.org/project/moto" + }, + { + "name": "motor", + "uri": "https://pypi.org/project/motor" + }, + { + "name": "mouseinfo", + "uri": "https://pypi.org/project/mouseinfo" + }, + { + "name": "moviepy", + "uri": "https://pypi.org/project/moviepy" + }, + { + "name": "mpmath", + "uri": "https://pypi.org/project/mpmath" + }, + { + "name": "msal", + "uri": "https://pypi.org/project/msal" + }, + { + "name": "msal-extensions", + "uri": "https://pypi.org/project/msal-extensions" + }, + { + "name": "msgpack", + "uri": "https://pypi.org/project/msgpack" + }, + { + "name": "msgpack-numpy", + "uri": "https://pypi.org/project/msgpack-numpy" + }, + { + "name": "msgpack-python", + "uri": "https://pypi.org/project/msgpack-python" + }, + { + "name": "msgraph-core", + "uri": "https://pypi.org/project/msgraph-core" + }, + { + "name": "msgraph-sdk", + "uri": "https://pypi.org/project/msgraph-sdk" + }, + { + "name": "msgspec", + "uri": "https://pypi.org/project/msgspec" + }, + { + "name": "msoffcrypto-tool", + "uri": "https://pypi.org/project/msoffcrypto-tool" + }, + { + "name": "msrest", + "uri": "https://pypi.org/project/msrest" + }, + { + "name": "msrestazure", + "uri": "https://pypi.org/project/msrestazure" + }, + { + "name": "mss", + "uri": "https://pypi.org/project/mss" + }, + { + "name": "multi-key-dict", + "uri": "https://pypi.org/project/multi-key-dict" + }, + { + "name": "multidict", + "uri": "https://pypi.org/project/multidict" + }, + { + "name": "multimapping", + "uri": "https://pypi.org/project/multimapping" + }, + { + "name": "multimethod", + "uri": "https://pypi.org/project/multimethod" + }, + { + "name": "multipart", + "uri": "https://pypi.org/project/multipart" + }, + { + "name": "multipledispatch", + "uri": "https://pypi.org/project/multipledispatch" + }, + { + "name": "multiprocess", + "uri": "https://pypi.org/project/multiprocess" + }, + { + "name": "multitasking", + "uri": "https://pypi.org/project/multitasking" + }, + { + "name": "multivolumefile", + "uri": "https://pypi.org/project/multivolumefile" + }, + { + "name": "munch", + "uri": "https://pypi.org/project/munch" + }, + { + "name": "munkres", + "uri": "https://pypi.org/project/munkres" + }, + { + "name": "murmurhash", + "uri": "https://pypi.org/project/murmurhash" + }, + { + "name": "mutagen", + "uri": "https://pypi.org/project/mutagen" + }, + { + "name": "mxnet", + "uri": "https://pypi.org/project/mxnet" + }, + { + "name": "mygene", + "uri": "https://pypi.org/project/mygene" + }, + { + "name": "mypy", + "uri": "https://pypi.org/project/mypy" + }, + { + "name": "mypy-boto3-apigateway", + "uri": "https://pypi.org/project/mypy-boto3-apigateway" + }, + { + "name": "mypy-boto3-appconfig", + "uri": "https://pypi.org/project/mypy-boto3-appconfig" + }, + { + "name": "mypy-boto3-appflow", + "uri": "https://pypi.org/project/mypy-boto3-appflow" + }, + { + "name": "mypy-boto3-athena", + "uri": "https://pypi.org/project/mypy-boto3-athena" + }, + { + "name": "mypy-boto3-cloudformation", + "uri": "https://pypi.org/project/mypy-boto3-cloudformation" + }, + { + "name": "mypy-boto3-dataexchange", + "uri": "https://pypi.org/project/mypy-boto3-dataexchange" + }, + { + "name": "mypy-boto3-dynamodb", + "uri": "https://pypi.org/project/mypy-boto3-dynamodb" + }, + { + "name": "mypy-boto3-ec2", + "uri": "https://pypi.org/project/mypy-boto3-ec2" + }, + { + "name": "mypy-boto3-ecr", + "uri": "https://pypi.org/project/mypy-boto3-ecr" + }, + { + "name": "mypy-boto3-events", + "uri": "https://pypi.org/project/mypy-boto3-events" + }, + { + "name": "mypy-boto3-glue", + "uri": "https://pypi.org/project/mypy-boto3-glue" + }, + { + "name": "mypy-boto3-iam", + "uri": "https://pypi.org/project/mypy-boto3-iam" + }, + { + "name": "mypy-boto3-kinesis", + "uri": "https://pypi.org/project/mypy-boto3-kinesis" + }, + { + "name": "mypy-boto3-lambda", + "uri": "https://pypi.org/project/mypy-boto3-lambda" + }, + { + "name": "mypy-boto3-rds", + "uri": "https://pypi.org/project/mypy-boto3-rds" + }, + { + "name": "mypy-boto3-redshift-data", + "uri": "https://pypi.org/project/mypy-boto3-redshift-data" + }, + { + "name": "mypy-boto3-s3", + "uri": "https://pypi.org/project/mypy-boto3-s3" + }, + { + "name": "mypy-boto3-schemas", + "uri": "https://pypi.org/project/mypy-boto3-schemas" + }, + { + "name": "mypy-boto3-secretsmanager", + "uri": "https://pypi.org/project/mypy-boto3-secretsmanager" + }, + { + "name": "mypy-boto3-signer", + "uri": "https://pypi.org/project/mypy-boto3-signer" + }, + { + "name": "mypy-boto3-sqs", + "uri": "https://pypi.org/project/mypy-boto3-sqs" + }, + { + "name": "mypy-boto3-ssm", + "uri": "https://pypi.org/project/mypy-boto3-ssm" + }, + { + "name": "mypy-boto3-stepfunctions", + "uri": "https://pypi.org/project/mypy-boto3-stepfunctions" + }, + { + "name": "mypy-boto3-sts", + "uri": "https://pypi.org/project/mypy-boto3-sts" + }, + { + "name": "mypy-boto3-xray", + "uri": "https://pypi.org/project/mypy-boto3-xray" + }, + { + "name": "mypy-extensions", + "uri": "https://pypi.org/project/mypy-extensions" + }, + { + "name": "mypy-protobuf", + "uri": "https://pypi.org/project/mypy-protobuf" + }, + { + "name": "mysql", + "uri": "https://pypi.org/project/mysql" + }, + { + "name": "mysql-connector", + "uri": "https://pypi.org/project/mysql-connector" + }, + { + "name": "mysql-connector-python", + "uri": "https://pypi.org/project/mysql-connector-python" + }, + { + "name": "mysqlclient", + "uri": "https://pypi.org/project/mysqlclient" + }, + { + "name": "myst-parser", + "uri": "https://pypi.org/project/myst-parser" + }, + { + "name": "naked", + "uri": "https://pypi.org/project/naked" + }, + { + "name": "nameparser", + "uri": "https://pypi.org/project/nameparser" + }, + { + "name": "namex", + "uri": "https://pypi.org/project/namex" + }, + { + "name": "nanoid", + "uri": "https://pypi.org/project/nanoid" + }, + { + "name": "narwhals", + "uri": "https://pypi.org/project/narwhals" + }, + { + "name": "natsort", + "uri": "https://pypi.org/project/natsort" + }, + { + "name": "natto-py", + "uri": "https://pypi.org/project/natto-py" + }, + { + "name": "nbclassic", + "uri": "https://pypi.org/project/nbclassic" + }, + { + "name": "nbclient", + "uri": "https://pypi.org/project/nbclient" + }, + { + "name": "nbconvert", + "uri": "https://pypi.org/project/nbconvert" + }, + { + "name": "nbformat", + "uri": "https://pypi.org/project/nbformat" + }, + { + "name": "nbsphinx", + "uri": "https://pypi.org/project/nbsphinx" + }, + { + "name": "ndg-httpsclient", + "uri": "https://pypi.org/project/ndg-httpsclient" + }, + { + "name": "ndindex", + "uri": "https://pypi.org/project/ndindex" + }, + { + "name": "ndjson", + "uri": "https://pypi.org/project/ndjson" + }, + { + "name": "neo4j", + "uri": "https://pypi.org/project/neo4j" + }, + { + "name": "nest-asyncio", + "uri": "https://pypi.org/project/nest-asyncio" + }, + { + "name": "netaddr", + "uri": "https://pypi.org/project/netaddr" + }, + { + "name": "netcdf4", + "uri": "https://pypi.org/project/netcdf4" + }, + { + "name": "netifaces", + "uri": "https://pypi.org/project/netifaces" + }, + { + "name": "netsuitesdk", + "uri": "https://pypi.org/project/netsuitesdk" + }, + { + "name": "networkx", + "uri": "https://pypi.org/project/networkx" + }, + { + "name": "newrelic", + "uri": "https://pypi.org/project/newrelic" + }, + { + "name": "newrelic-telemetry-sdk", + "uri": "https://pypi.org/project/newrelic-telemetry-sdk" + }, + { + "name": "nh3", + "uri": "https://pypi.org/project/nh3" + }, + { + "name": "nibabel", + "uri": "https://pypi.org/project/nibabel" + }, + { + "name": "ninja", + "uri": "https://pypi.org/project/ninja" + }, + { + "name": "nltk", + "uri": "https://pypi.org/project/nltk" + }, + { + "name": "node-semver", + "uri": "https://pypi.org/project/node-semver" + }, + { + "name": "nodeenv", + "uri": "https://pypi.org/project/nodeenv" + }, + { + "name": "nose", + "uri": "https://pypi.org/project/nose" + }, + { + "name": "nose2", + "uri": "https://pypi.org/project/nose2" + }, + { + "name": "notebook", + "uri": "https://pypi.org/project/notebook" + }, + { + "name": "notebook-shim", + "uri": "https://pypi.org/project/notebook-shim" + }, + { + "name": "notifiers", + "uri": "https://pypi.org/project/notifiers" + }, + { + "name": "notion-client", + "uri": "https://pypi.org/project/notion-client" + }, + { + "name": "nox", + "uri": "https://pypi.org/project/nox" + }, + { + "name": "nptyping", + "uri": "https://pypi.org/project/nptyping" + }, + { + "name": "ntlm-auth", + "uri": "https://pypi.org/project/ntlm-auth" + }, + { + "name": "ntplib", + "uri": "https://pypi.org/project/ntplib" + }, + { + "name": "nulltype", + "uri": "https://pypi.org/project/nulltype" + }, + { + "name": "num2words", + "uri": "https://pypi.org/project/num2words" + }, + { + "name": "numba", + "uri": "https://pypi.org/project/numba" + }, + { + "name": "numcodecs", + "uri": "https://pypi.org/project/numcodecs" + }, + { + "name": "numdifftools", + "uri": "https://pypi.org/project/numdifftools" + }, + { + "name": "numexpr", + "uri": "https://pypi.org/project/numexpr" + }, + { + "name": "numpy", + "uri": "https://pypi.org/project/numpy" + }, + { + "name": "numpy-financial", + "uri": "https://pypi.org/project/numpy-financial" + }, + { + "name": "numpy-quaternion", + "uri": "https://pypi.org/project/numpy-quaternion" + }, + { + "name": "numpydoc", + "uri": "https://pypi.org/project/numpydoc" + }, + { + "name": "nvidia-cublas-cu11", + "uri": "https://pypi.org/project/nvidia-cublas-cu11" + }, + { + "name": "nvidia-cublas-cu12", + "uri": "https://pypi.org/project/nvidia-cublas-cu12" + }, + { + "name": "nvidia-cuda-cupti-cu11", + "uri": "https://pypi.org/project/nvidia-cuda-cupti-cu11" + }, + { + "name": "nvidia-cuda-cupti-cu12", + "uri": "https://pypi.org/project/nvidia-cuda-cupti-cu12" + }, + { + "name": "nvidia-cuda-nvrtc-cu11", + "uri": "https://pypi.org/project/nvidia-cuda-nvrtc-cu11" + }, + { + "name": "nvidia-cuda-nvrtc-cu12", + "uri": "https://pypi.org/project/nvidia-cuda-nvrtc-cu12" + }, + { + "name": "nvidia-cuda-runtime-cu11", + "uri": "https://pypi.org/project/nvidia-cuda-runtime-cu11" + }, + { + "name": "nvidia-cuda-runtime-cu12", + "uri": "https://pypi.org/project/nvidia-cuda-runtime-cu12" + }, + { + "name": "nvidia-cudnn-cu11", + "uri": "https://pypi.org/project/nvidia-cudnn-cu11" + }, + { + "name": "nvidia-cudnn-cu12", + "uri": "https://pypi.org/project/nvidia-cudnn-cu12" + }, + { + "name": "nvidia-cufft-cu11", + "uri": "https://pypi.org/project/nvidia-cufft-cu11" + }, + { + "name": "nvidia-cufft-cu12", + "uri": "https://pypi.org/project/nvidia-cufft-cu12" + }, + { + "name": "nvidia-curand-cu11", + "uri": "https://pypi.org/project/nvidia-curand-cu11" + }, + { + "name": "nvidia-curand-cu12", + "uri": "https://pypi.org/project/nvidia-curand-cu12" + }, + { + "name": "nvidia-cusolver-cu11", + "uri": "https://pypi.org/project/nvidia-cusolver-cu11" + }, + { + "name": "nvidia-cusolver-cu12", + "uri": "https://pypi.org/project/nvidia-cusolver-cu12" + }, + { + "name": "nvidia-cusparse-cu11", + "uri": "https://pypi.org/project/nvidia-cusparse-cu11" + }, + { + "name": "nvidia-cusparse-cu12", + "uri": "https://pypi.org/project/nvidia-cusparse-cu12" + }, + { + "name": "nvidia-ml-py", + "uri": "https://pypi.org/project/nvidia-ml-py" + }, + { + "name": "nvidia-nccl-cu11", + "uri": "https://pypi.org/project/nvidia-nccl-cu11" + }, + { + "name": "nvidia-nccl-cu12", + "uri": "https://pypi.org/project/nvidia-nccl-cu12" + }, + { + "name": "nvidia-nvjitlink-cu12", + "uri": "https://pypi.org/project/nvidia-nvjitlink-cu12" + }, + { + "name": "nvidia-nvtx-cu11", + "uri": "https://pypi.org/project/nvidia-nvtx-cu11" + }, + { + "name": "nvidia-nvtx-cu12", + "uri": "https://pypi.org/project/nvidia-nvtx-cu12" + }, + { + "name": "o365", + "uri": "https://pypi.org/project/o365" + }, + { + "name": "oauth2client", + "uri": "https://pypi.org/project/oauth2client" + }, + { + "name": "oauthlib", + "uri": "https://pypi.org/project/oauthlib" + }, + { + "name": "objsize", + "uri": "https://pypi.org/project/objsize" + }, + { + "name": "oci", + "uri": "https://pypi.org/project/oci" + }, + { + "name": "odfpy", + "uri": "https://pypi.org/project/odfpy" + }, + { + "name": "office365-rest-python-client", + "uri": "https://pypi.org/project/office365-rest-python-client" + }, + { + "name": "okta", + "uri": "https://pypi.org/project/okta" + }, + { + "name": "oldest-supported-numpy", + "uri": "https://pypi.org/project/oldest-supported-numpy" + }, + { + "name": "olefile", + "uri": "https://pypi.org/project/olefile" + }, + { + "name": "omegaconf", + "uri": "https://pypi.org/project/omegaconf" + }, + { + "name": "onnx", + "uri": "https://pypi.org/project/onnx" + }, + { + "name": "onnxconverter-common", + "uri": "https://pypi.org/project/onnxconverter-common" + }, + { + "name": "onnxruntime", + "uri": "https://pypi.org/project/onnxruntime" + }, + { + "name": "onnxruntime-gpu", + "uri": "https://pypi.org/project/onnxruntime-gpu" + }, + { + "name": "open-clip-torch", + "uri": "https://pypi.org/project/open-clip-torch" + }, + { + "name": "open3d", + "uri": "https://pypi.org/project/open3d" + }, + { + "name": "openai", + "uri": "https://pypi.org/project/openai" + }, + { + "name": "openapi-schema-pydantic", + "uri": "https://pypi.org/project/openapi-schema-pydantic" + }, + { + "name": "openapi-schema-validator", + "uri": "https://pypi.org/project/openapi-schema-validator" + }, + { + "name": "openapi-spec-validator", + "uri": "https://pypi.org/project/openapi-spec-validator" + }, + { + "name": "opencensus", + "uri": "https://pypi.org/project/opencensus" + }, + { + "name": "opencensus-context", + "uri": "https://pypi.org/project/opencensus-context" + }, + { + "name": "opencensus-ext-azure", + "uri": "https://pypi.org/project/opencensus-ext-azure" + }, + { + "name": "opencensus-ext-logging", + "uri": "https://pypi.org/project/opencensus-ext-logging" + }, + { + "name": "opencv-contrib-python", + "uri": "https://pypi.org/project/opencv-contrib-python" + }, + { + "name": "opencv-contrib-python-headless", + "uri": "https://pypi.org/project/opencv-contrib-python-headless" + }, + { + "name": "opencv-python", + "uri": "https://pypi.org/project/opencv-python" + }, + { + "name": "opencv-python-headless", + "uri": "https://pypi.org/project/opencv-python-headless" + }, + { + "name": "openinference-instrumentation", + "uri": "https://pypi.org/project/openinference-instrumentation" + }, + { + "name": "openinference-semantic-conventions", + "uri": "https://pypi.org/project/openinference-semantic-conventions" + }, + { + "name": "openlineage-airflow", + "uri": "https://pypi.org/project/openlineage-airflow" + }, + { + "name": "openlineage-integration-common", + "uri": "https://pypi.org/project/openlineage-integration-common" + }, + { + "name": "openlineage-python", + "uri": "https://pypi.org/project/openlineage-python" + }, + { + "name": "openlineage-sql", + "uri": "https://pypi.org/project/openlineage-sql" + }, + { + "name": "openpyxl", + "uri": "https://pypi.org/project/openpyxl" + }, + { + "name": "opensearch-py", + "uri": "https://pypi.org/project/opensearch-py" + }, + { + "name": "openstacksdk", + "uri": "https://pypi.org/project/openstacksdk" + }, + { + "name": "opentelemetry-api", + "uri": "https://pypi.org/project/opentelemetry-api" + }, + { + "name": "opentelemetry-distro", + "uri": "https://pypi.org/project/opentelemetry-distro" + }, + { + "name": "opentelemetry-exporter-gcp-trace", + "uri": "https://pypi.org/project/opentelemetry-exporter-gcp-trace" + }, + { + "name": "opentelemetry-exporter-otlp", + "uri": "https://pypi.org/project/opentelemetry-exporter-otlp" + }, + { + "name": "opentelemetry-exporter-otlp-proto-common", + "uri": "https://pypi.org/project/opentelemetry-exporter-otlp-proto-common" + }, + { + "name": "opentelemetry-exporter-otlp-proto-grpc", + "uri": "https://pypi.org/project/opentelemetry-exporter-otlp-proto-grpc" + }, + { + "name": "opentelemetry-exporter-otlp-proto-http", + "uri": "https://pypi.org/project/opentelemetry-exporter-otlp-proto-http" + }, + { + "name": "opentelemetry-instrumentation", + "uri": "https://pypi.org/project/opentelemetry-instrumentation" + }, + { + "name": "opentelemetry-instrumentation-aiohttp-client", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-aiohttp-client" + }, + { + "name": "opentelemetry-instrumentation-asgi", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-asgi" + }, + { + "name": "opentelemetry-instrumentation-aws-lambda", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-aws-lambda" + }, + { + "name": "opentelemetry-instrumentation-botocore", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-botocore" + }, + { + "name": "opentelemetry-instrumentation-dbapi", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-dbapi" + }, + { + "name": "opentelemetry-instrumentation-django", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-django" + }, + { + "name": "opentelemetry-instrumentation-fastapi", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-fastapi" + }, + { + "name": "opentelemetry-instrumentation-flask", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-flask" + }, + { + "name": "opentelemetry-instrumentation-grpc", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-grpc" + }, + { + "name": "opentelemetry-instrumentation-httpx", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-httpx" + }, + { + "name": "opentelemetry-instrumentation-jinja2", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-jinja2" + }, + { + "name": "opentelemetry-instrumentation-logging", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-logging" + }, + { + "name": "opentelemetry-instrumentation-psycopg2", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-psycopg2" + }, + { + "name": "opentelemetry-instrumentation-redis", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-redis" + }, + { + "name": "opentelemetry-instrumentation-requests", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-requests" + }, + { + "name": "opentelemetry-instrumentation-sqlalchemy", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-sqlalchemy" + }, + { + "name": "opentelemetry-instrumentation-sqlite3", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-sqlite3" + }, + { + "name": "opentelemetry-instrumentation-urllib", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-urllib" + }, + { + "name": "opentelemetry-instrumentation-urllib3", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-urllib3" + }, + { + "name": "opentelemetry-instrumentation-wsgi", + "uri": "https://pypi.org/project/opentelemetry-instrumentation-wsgi" + }, + { + "name": "opentelemetry-propagator-aws-xray", + "uri": "https://pypi.org/project/opentelemetry-propagator-aws-xray" + }, + { + "name": "opentelemetry-propagator-b3", + "uri": "https://pypi.org/project/opentelemetry-propagator-b3" + }, + { + "name": "opentelemetry-proto", + "uri": "https://pypi.org/project/opentelemetry-proto" + }, + { + "name": "opentelemetry-resource-detector-azure", + "uri": "https://pypi.org/project/opentelemetry-resource-detector-azure" + }, + { + "name": "opentelemetry-resourcedetector-gcp", + "uri": "https://pypi.org/project/opentelemetry-resourcedetector-gcp" + }, + { + "name": "opentelemetry-sdk", + "uri": "https://pypi.org/project/opentelemetry-sdk" + }, + { + "name": "opentelemetry-sdk-extension-aws", + "uri": "https://pypi.org/project/opentelemetry-sdk-extension-aws" + }, + { + "name": "opentelemetry-semantic-conventions", + "uri": "https://pypi.org/project/opentelemetry-semantic-conventions" + }, + { + "name": "opentelemetry-util-http", + "uri": "https://pypi.org/project/opentelemetry-util-http" + }, + { + "name": "opentracing", + "uri": "https://pypi.org/project/opentracing" + }, + { + "name": "openturns", + "uri": "https://pypi.org/project/openturns" + }, + { + "name": "openvino", + "uri": "https://pypi.org/project/openvino" + }, + { + "name": "openvino-telemetry", + "uri": "https://pypi.org/project/openvino-telemetry" + }, + { + "name": "openxlab", + "uri": "https://pypi.org/project/openxlab" + }, + { + "name": "opsgenie-sdk", + "uri": "https://pypi.org/project/opsgenie-sdk" + }, + { + "name": "opt-einsum", + "uri": "https://pypi.org/project/opt-einsum" + }, + { + "name": "optax", + "uri": "https://pypi.org/project/optax" + }, + { + "name": "optimum", + "uri": "https://pypi.org/project/optimum" + }, + { + "name": "optree", + "uri": "https://pypi.org/project/optree" + }, + { + "name": "optuna", + "uri": "https://pypi.org/project/optuna" + }, + { + "name": "oracledb", + "uri": "https://pypi.org/project/oracledb" + }, + { + "name": "orbax-checkpoint", + "uri": "https://pypi.org/project/orbax-checkpoint" + }, + { + "name": "ordered-set", + "uri": "https://pypi.org/project/ordered-set" + }, + { + "name": "orderedmultidict", + "uri": "https://pypi.org/project/orderedmultidict" + }, + { + "name": "orderly-set", + "uri": "https://pypi.org/project/orderly-set" + }, + { + "name": "orjson", + "uri": "https://pypi.org/project/orjson" + }, + { + "name": "ortools", + "uri": "https://pypi.org/project/ortools" + }, + { + "name": "os-service-types", + "uri": "https://pypi.org/project/os-service-types" + }, + { + "name": "oscrypto", + "uri": "https://pypi.org/project/oscrypto" + }, + { + "name": "oslo-config", + "uri": "https://pypi.org/project/oslo-config" + }, + { + "name": "oslo-i18n", + "uri": "https://pypi.org/project/oslo-i18n" + }, + { + "name": "oslo-serialization", + "uri": "https://pypi.org/project/oslo-serialization" + }, + { + "name": "oslo-utils", + "uri": "https://pypi.org/project/oslo-utils" + }, + { + "name": "osmium", + "uri": "https://pypi.org/project/osmium" + }, + { + "name": "osqp", + "uri": "https://pypi.org/project/osqp" + }, + { + "name": "oss2", + "uri": "https://pypi.org/project/oss2" + }, + { + "name": "outcome", + "uri": "https://pypi.org/project/outcome" + }, + { + "name": "outlines", + "uri": "https://pypi.org/project/outlines" + }, + { + "name": "overrides", + "uri": "https://pypi.org/project/overrides" + }, + { + "name": "oyaml", + "uri": "https://pypi.org/project/oyaml" + }, + { + "name": "p4python", + "uri": "https://pypi.org/project/p4python" + }, + { + "name": "packageurl-python", + "uri": "https://pypi.org/project/packageurl-python" + }, + { + "name": "packaging", + "uri": "https://pypi.org/project/packaging" + }, + { + "name": "paddleocr", + "uri": "https://pypi.org/project/paddleocr" + }, + { + "name": "paginate", + "uri": "https://pypi.org/project/paginate" + }, + { + "name": "paho-mqtt", + "uri": "https://pypi.org/project/paho-mqtt" + }, + { + "name": "palettable", + "uri": "https://pypi.org/project/palettable" + }, + { + "name": "pamqp", + "uri": "https://pypi.org/project/pamqp" + }, + { + "name": "pandas", + "uri": "https://pypi.org/project/pandas" + }, + { + "name": "pandas-gbq", + "uri": "https://pypi.org/project/pandas-gbq" + }, + { + "name": "pandas-stubs", + "uri": "https://pypi.org/project/pandas-stubs" + }, + { + "name": "pandasql", + "uri": "https://pypi.org/project/pandasql" + }, + { + "name": "pandera", + "uri": "https://pypi.org/project/pandera" + }, + { + "name": "pandocfilters", + "uri": "https://pypi.org/project/pandocfilters" + }, + { + "name": "panel", + "uri": "https://pypi.org/project/panel" + }, + { + "name": "pantab", + "uri": "https://pypi.org/project/pantab" + }, + { + "name": "papermill", + "uri": "https://pypi.org/project/papermill" + }, + { + "name": "param", + "uri": "https://pypi.org/project/param" + }, + { + "name": "parameterized", + "uri": "https://pypi.org/project/parameterized" + }, + { + "name": "paramiko", + "uri": "https://pypi.org/project/paramiko" + }, + { + "name": "parse", + "uri": "https://pypi.org/project/parse" + }, + { + "name": "parse-type", + "uri": "https://pypi.org/project/parse-type" + }, + { + "name": "parsedatetime", + "uri": "https://pypi.org/project/parsedatetime" + }, + { + "name": "parsel", + "uri": "https://pypi.org/project/parsel" + }, + { + "name": "parsimonious", + "uri": "https://pypi.org/project/parsimonious" + }, + { + "name": "parsley", + "uri": "https://pypi.org/project/parsley" + }, + { + "name": "parso", + "uri": "https://pypi.org/project/parso" + }, + { + "name": "partd", + "uri": "https://pypi.org/project/partd" + }, + { + "name": "parver", + "uri": "https://pypi.org/project/parver" + }, + { + "name": "passlib", + "uri": "https://pypi.org/project/passlib" + }, + { + "name": "paste", + "uri": "https://pypi.org/project/paste" + }, + { + "name": "pastedeploy", + "uri": "https://pypi.org/project/pastedeploy" + }, + { + "name": "pastel", + "uri": "https://pypi.org/project/pastel" + }, + { + "name": "patch-ng", + "uri": "https://pypi.org/project/patch-ng" + }, + { + "name": "patchelf", + "uri": "https://pypi.org/project/patchelf" + }, + { + "name": "path", + "uri": "https://pypi.org/project/path" + }, + { + "name": "path-dict", + "uri": "https://pypi.org/project/path-dict" + }, + { + "name": "pathable", + "uri": "https://pypi.org/project/pathable" + }, + { + "name": "pathlib", + "uri": "https://pypi.org/project/pathlib" + }, + { + "name": "pathlib-abc", + "uri": "https://pypi.org/project/pathlib-abc" + }, + { + "name": "pathlib-mate", + "uri": "https://pypi.org/project/pathlib-mate" + }, + { + "name": "pathlib2", + "uri": "https://pypi.org/project/pathlib2" + }, + { + "name": "pathos", + "uri": "https://pypi.org/project/pathos" + }, + { + "name": "pathspec", + "uri": "https://pypi.org/project/pathspec" + }, + { + "name": "pathtools", + "uri": "https://pypi.org/project/pathtools" + }, + { + "name": "pathvalidate", + "uri": "https://pypi.org/project/pathvalidate" + }, + { + "name": "pathy", + "uri": "https://pypi.org/project/pathy" + }, + { + "name": "patool", + "uri": "https://pypi.org/project/patool" + }, + { + "name": "patsy", + "uri": "https://pypi.org/project/patsy" + }, + { + "name": "pbr", + "uri": "https://pypi.org/project/pbr" + }, + { + "name": "pbs-installer", + "uri": "https://pypi.org/project/pbs-installer" + }, + { + "name": "pdb2pqr", + "uri": "https://pypi.org/project/pdb2pqr" + }, + { + "name": "pdf2image", + "uri": "https://pypi.org/project/pdf2image" + }, + { + "name": "pdfkit", + "uri": "https://pypi.org/project/pdfkit" + }, + { + "name": "pdfminer-six", + "uri": "https://pypi.org/project/pdfminer-six" + }, + { + "name": "pdfplumber", + "uri": "https://pypi.org/project/pdfplumber" + }, + { + "name": "pdm", + "uri": "https://pypi.org/project/pdm" + }, + { + "name": "pdpyras", + "uri": "https://pypi.org/project/pdpyras" + }, + { + "name": "peewee", + "uri": "https://pypi.org/project/peewee" + }, + { + "name": "pefile", + "uri": "https://pypi.org/project/pefile" + }, + { + "name": "peft", + "uri": "https://pypi.org/project/peft" + }, + { + "name": "pendulum", + "uri": "https://pypi.org/project/pendulum" + }, + { + "name": "pentapy", + "uri": "https://pypi.org/project/pentapy" + }, + { + "name": "pep517", + "uri": "https://pypi.org/project/pep517" + }, + { + "name": "pep8", + "uri": "https://pypi.org/project/pep8" + }, + { + "name": "pep8-naming", + "uri": "https://pypi.org/project/pep8-naming" + }, + { + "name": "peppercorn", + "uri": "https://pypi.org/project/peppercorn" + }, + { + "name": "persistence", + "uri": "https://pypi.org/project/persistence" + }, + { + "name": "persistent", + "uri": "https://pypi.org/project/persistent" + }, + { + "name": "pex", + "uri": "https://pypi.org/project/pex" + }, + { + "name": "pexpect", + "uri": "https://pypi.org/project/pexpect" + }, + { + "name": "pfzy", + "uri": "https://pypi.org/project/pfzy" + }, + { + "name": "pg8000", + "uri": "https://pypi.org/project/pg8000" + }, + { + "name": "pgpy", + "uri": "https://pypi.org/project/pgpy" + }, + { + "name": "pgvector", + "uri": "https://pypi.org/project/pgvector" + }, + { + "name": "phik", + "uri": "https://pypi.org/project/phik" + }, + { + "name": "phonenumbers", + "uri": "https://pypi.org/project/phonenumbers" + }, + { + "name": "phonenumberslite", + "uri": "https://pypi.org/project/phonenumberslite" + }, + { + "name": "pickleshare", + "uri": "https://pypi.org/project/pickleshare" + }, + { + "name": "piexif", + "uri": "https://pypi.org/project/piexif" + }, + { + "name": "pika", + "uri": "https://pypi.org/project/pika" + }, + { + "name": "pikepdf", + "uri": "https://pypi.org/project/pikepdf" + }, + { + "name": "pillow", + "uri": "https://pypi.org/project/pillow" + }, + { + "name": "pillow-avif-plugin", + "uri": "https://pypi.org/project/pillow-avif-plugin" + }, + { + "name": "pillow-heif", + "uri": "https://pypi.org/project/pillow-heif" + }, + { + "name": "pinecone-client", + "uri": "https://pypi.org/project/pinecone-client" + }, + { + "name": "pint", + "uri": "https://pypi.org/project/pint" + }, + { + "name": "pip", + "uri": "https://pypi.org/project/pip" + }, + { + "name": "pip-api", + "uri": "https://pypi.org/project/pip-api" + }, + { + "name": "pip-requirements-parser", + "uri": "https://pypi.org/project/pip-requirements-parser" + }, + { + "name": "pip-tools", + "uri": "https://pypi.org/project/pip-tools" + }, + { + "name": "pipdeptree", + "uri": "https://pypi.org/project/pipdeptree" + }, + { + "name": "pipelinewise-singer-python", + "uri": "https://pypi.org/project/pipelinewise-singer-python" + }, + { + "name": "pipenv", + "uri": "https://pypi.org/project/pipenv" + }, + { + "name": "pipreqs", + "uri": "https://pypi.org/project/pipreqs" + }, + { + "name": "pipx", + "uri": "https://pypi.org/project/pipx" + }, + { + "name": "pkce", + "uri": "https://pypi.org/project/pkce" + }, + { + "name": "pkgconfig", + "uri": "https://pypi.org/project/pkgconfig" + }, + { + "name": "pkginfo", + "uri": "https://pypi.org/project/pkginfo" + }, + { + "name": "pkgutil-resolve-name", + "uri": "https://pypi.org/project/pkgutil-resolve-name" + }, + { + "name": "plac", + "uri": "https://pypi.org/project/plac" + }, + { + "name": "plaster", + "uri": "https://pypi.org/project/plaster" + }, + { + "name": "plaster-pastedeploy", + "uri": "https://pypi.org/project/plaster-pastedeploy" + }, + { + "name": "platformdirs", + "uri": "https://pypi.org/project/platformdirs" + }, + { + "name": "playwright", + "uri": "https://pypi.org/project/playwright" + }, + { + "name": "plotly", + "uri": "https://pypi.org/project/plotly" + }, + { + "name": "plotnine", + "uri": "https://pypi.org/project/plotnine" + }, + { + "name": "pluggy", + "uri": "https://pypi.org/project/pluggy" + }, + { + "name": "pluginbase", + "uri": "https://pypi.org/project/pluginbase" + }, + { + "name": "plumbum", + "uri": "https://pypi.org/project/plumbum" + }, + { + "name": "ply", + "uri": "https://pypi.org/project/ply" + }, + { + "name": "pmdarima", + "uri": "https://pypi.org/project/pmdarima" + }, + { + "name": "poetry", + "uri": "https://pypi.org/project/poetry" + }, + { + "name": "poetry-core", + "uri": "https://pypi.org/project/poetry-core" + }, + { + "name": "poetry-dynamic-versioning", + "uri": "https://pypi.org/project/poetry-dynamic-versioning" + }, + { + "name": "poetry-plugin-export", + "uri": "https://pypi.org/project/poetry-plugin-export" + }, + { + "name": "poetry-plugin-pypi-mirror", + "uri": "https://pypi.org/project/poetry-plugin-pypi-mirror" + }, + { + "name": "polars", + "uri": "https://pypi.org/project/polars" + }, + { + "name": "polib", + "uri": "https://pypi.org/project/polib" + }, + { + "name": "policy-sentry", + "uri": "https://pypi.org/project/policy-sentry" + }, + { + "name": "polling", + "uri": "https://pypi.org/project/polling" + }, + { + "name": "polling2", + "uri": "https://pypi.org/project/polling2" + }, + { + "name": "polyline", + "uri": "https://pypi.org/project/polyline" + }, + { + "name": "pony", + "uri": "https://pypi.org/project/pony" + }, + { + "name": "pooch", + "uri": "https://pypi.org/project/pooch" + }, + { + "name": "port-for", + "uri": "https://pypi.org/project/port-for" + }, + { + "name": "portalocker", + "uri": "https://pypi.org/project/portalocker" + }, + { + "name": "portend", + "uri": "https://pypi.org/project/portend" + }, + { + "name": "portpicker", + "uri": "https://pypi.org/project/portpicker" + }, + { + "name": "posthog", + "uri": "https://pypi.org/project/posthog" + }, + { + "name": "pox", + "uri": "https://pypi.org/project/pox" + }, + { + "name": "ppft", + "uri": "https://pypi.org/project/ppft" + }, + { + "name": "pprintpp", + "uri": "https://pypi.org/project/pprintpp" + }, + { + "name": "prance", + "uri": "https://pypi.org/project/prance" + }, + { + "name": "pre-commit", + "uri": "https://pypi.org/project/pre-commit" + }, + { + "name": "pre-commit-hooks", + "uri": "https://pypi.org/project/pre-commit-hooks" + }, + { + "name": "prefect", + "uri": "https://pypi.org/project/prefect" + }, + { + "name": "prefect-aws", + "uri": "https://pypi.org/project/prefect-aws" + }, + { + "name": "prefect-gcp", + "uri": "https://pypi.org/project/prefect-gcp" + }, + { + "name": "premailer", + "uri": "https://pypi.org/project/premailer" + }, + { + "name": "preshed", + "uri": "https://pypi.org/project/preshed" + }, + { + "name": "presto-python-client", + "uri": "https://pypi.org/project/presto-python-client" + }, + { + "name": "pretend", + "uri": "https://pypi.org/project/pretend" + }, + { + "name": "pretty-html-table", + "uri": "https://pypi.org/project/pretty-html-table" + }, + { + "name": "prettytable", + "uri": "https://pypi.org/project/prettytable" + }, + { + "name": "primepy", + "uri": "https://pypi.org/project/primepy" + }, + { + "name": "priority", + "uri": "https://pypi.org/project/priority" + }, + { + "name": "prisma", + "uri": "https://pypi.org/project/prisma" + }, + { + "name": "prison", + "uri": "https://pypi.org/project/prison" + }, + { + "name": "probableparsing", + "uri": "https://pypi.org/project/probableparsing" + }, + { + "name": "proglog", + "uri": "https://pypi.org/project/proglog" + }, + { + "name": "progress", + "uri": "https://pypi.org/project/progress" + }, + { + "name": "progressbar2", + "uri": "https://pypi.org/project/progressbar2" + }, + { + "name": "prometheus-client", + "uri": "https://pypi.org/project/prometheus-client" + }, + { + "name": "prometheus-fastapi-instrumentator", + "uri": "https://pypi.org/project/prometheus-fastapi-instrumentator" + }, + { + "name": "prometheus-flask-exporter", + "uri": "https://pypi.org/project/prometheus-flask-exporter" + }, + { + "name": "promise", + "uri": "https://pypi.org/project/promise" + }, + { + "name": "prompt-toolkit", + "uri": "https://pypi.org/project/prompt-toolkit" + }, + { + "name": "pronouncing", + "uri": "https://pypi.org/project/pronouncing" + }, + { + "name": "property-manager", + "uri": "https://pypi.org/project/property-manager" + }, + { + "name": "prophet", + "uri": "https://pypi.org/project/prophet" + }, + { + "name": "propka", + "uri": "https://pypi.org/project/propka" + }, + { + "name": "prospector", + "uri": "https://pypi.org/project/prospector" + }, + { + "name": "protego", + "uri": "https://pypi.org/project/protego" + }, + { + "name": "proto-plus", + "uri": "https://pypi.org/project/proto-plus" + }, + { + "name": "protobuf", + "uri": "https://pypi.org/project/protobuf" + }, + { + "name": "protobuf3-to-dict", + "uri": "https://pypi.org/project/protobuf3-to-dict" + }, + { + "name": "psutil", + "uri": "https://pypi.org/project/psutil" + }, + { + "name": "psycopg", + "uri": "https://pypi.org/project/psycopg" + }, + { + "name": "psycopg-binary", + "uri": "https://pypi.org/project/psycopg-binary" + }, + { + "name": "psycopg-pool", + "uri": "https://pypi.org/project/psycopg-pool" + }, + { + "name": "psycopg2", + "uri": "https://pypi.org/project/psycopg2" + }, + { + "name": "psycopg2-binary", + "uri": "https://pypi.org/project/psycopg2-binary" + }, + { + "name": "ptpython", + "uri": "https://pypi.org/project/ptpython" + }, + { + "name": "ptyprocess", + "uri": "https://pypi.org/project/ptyprocess" + }, + { + "name": "publication", + "uri": "https://pypi.org/project/publication" + }, + { + "name": "publicsuffix2", + "uri": "https://pypi.org/project/publicsuffix2" + }, + { + "name": "publish-event-sns", + "uri": "https://pypi.org/project/publish-event-sns" + }, + { + "name": "pulp", + "uri": "https://pypi.org/project/pulp" + }, + { + "name": "pulsar-client", + "uri": "https://pypi.org/project/pulsar-client" + }, + { + "name": "pulumi", + "uri": "https://pypi.org/project/pulumi" + }, + { + "name": "pure-eval", + "uri": "https://pypi.org/project/pure-eval" + }, + { + "name": "pure-sasl", + "uri": "https://pypi.org/project/pure-sasl" + }, + { + "name": "pusher", + "uri": "https://pypi.org/project/pusher" + }, + { + "name": "pvlib", + "uri": "https://pypi.org/project/pvlib" + }, + { + "name": "py", + "uri": "https://pypi.org/project/py" + }, + { + "name": "py-cpuinfo", + "uri": "https://pypi.org/project/py-cpuinfo" + }, + { + "name": "py-models-parser", + "uri": "https://pypi.org/project/py-models-parser" + }, + { + "name": "py-partiql-parser", + "uri": "https://pypi.org/project/py-partiql-parser" + }, + { + "name": "py-serializable", + "uri": "https://pypi.org/project/py-serializable" + }, + { + "name": "py-spy", + "uri": "https://pypi.org/project/py-spy" + }, + { + "name": "py4j", + "uri": "https://pypi.org/project/py4j" + }, + { + "name": "py7zr", + "uri": "https://pypi.org/project/py7zr" + }, + { + "name": "pyaes", + "uri": "https://pypi.org/project/pyaes" + }, + { + "name": "pyahocorasick", + "uri": "https://pypi.org/project/pyahocorasick" + }, + { + "name": "pyairports", + "uri": "https://pypi.org/project/pyairports" + }, + { + "name": "pyairtable", + "uri": "https://pypi.org/project/pyairtable" + }, + { + "name": "pyaml", + "uri": "https://pypi.org/project/pyaml" + }, + { + "name": "pyannote-audio", + "uri": "https://pypi.org/project/pyannote-audio" + }, + { + "name": "pyannote-core", + "uri": "https://pypi.org/project/pyannote-core" + }, + { + "name": "pyannote-database", + "uri": "https://pypi.org/project/pyannote-database" + }, + { + "name": "pyannote-metrics", + "uri": "https://pypi.org/project/pyannote-metrics" + }, + { + "name": "pyannote-pipeline", + "uri": "https://pypi.org/project/pyannote-pipeline" + }, + { + "name": "pyapacheatlas", + "uri": "https://pypi.org/project/pyapacheatlas" + }, + { + "name": "pyarrow", + "uri": "https://pypi.org/project/pyarrow" + }, + { + "name": "pyarrow-hotfix", + "uri": "https://pypi.org/project/pyarrow-hotfix" + }, + { + "name": "pyasn1", + "uri": "https://pypi.org/project/pyasn1" + }, + { + "name": "pyasn1-modules", + "uri": "https://pypi.org/project/pyasn1-modules" + }, + { + "name": "pyathena", + "uri": "https://pypi.org/project/pyathena" + }, + { + "name": "pyautogui", + "uri": "https://pypi.org/project/pyautogui" + }, + { + "name": "pyawscron", + "uri": "https://pypi.org/project/pyawscron" + }, + { + "name": "pybase64", + "uri": "https://pypi.org/project/pybase64" + }, + { + "name": "pybcj", + "uri": "https://pypi.org/project/pybcj" + }, + { + "name": "pybind11", + "uri": "https://pypi.org/project/pybind11" + }, + { + "name": "pybloom-live", + "uri": "https://pypi.org/project/pybloom-live" + }, + { + "name": "pybtex", + "uri": "https://pypi.org/project/pybtex" + }, + { + "name": "pybytebuffer", + "uri": "https://pypi.org/project/pybytebuffer" + }, + { + "name": "pycairo", + "uri": "https://pypi.org/project/pycairo" + }, + { + "name": "pycares", + "uri": "https://pypi.org/project/pycares" + }, + { + "name": "pycep-parser", + "uri": "https://pypi.org/project/pycep-parser" + }, + { + "name": "pyclipper", + "uri": "https://pypi.org/project/pyclipper" + }, + { + "name": "pyclothoids", + "uri": "https://pypi.org/project/pyclothoids" + }, + { + "name": "pycocotools", + "uri": "https://pypi.org/project/pycocotools" + }, + { + "name": "pycodestyle", + "uri": "https://pypi.org/project/pycodestyle" + }, + { + "name": "pycomposefile", + "uri": "https://pypi.org/project/pycomposefile" + }, + { + "name": "pycountry", + "uri": "https://pypi.org/project/pycountry" + }, + { + "name": "pycparser", + "uri": "https://pypi.org/project/pycparser" + }, + { + "name": "pycrypto", + "uri": "https://pypi.org/project/pycrypto" + }, + { + "name": "pycryptodome", + "uri": "https://pypi.org/project/pycryptodome" + }, + { + "name": "pycryptodomex", + "uri": "https://pypi.org/project/pycryptodomex" + }, + { + "name": "pycurl", + "uri": "https://pypi.org/project/pycurl" + }, + { + "name": "pydantic", + "uri": "https://pypi.org/project/pydantic" + }, + { + "name": "pydantic-core", + "uri": "https://pypi.org/project/pydantic-core" + }, + { + "name": "pydantic-extra-types", + "uri": "https://pypi.org/project/pydantic-extra-types" + }, + { + "name": "pydantic-openapi-helper", + "uri": "https://pypi.org/project/pydantic-openapi-helper" + }, + { + "name": "pydantic-settings", + "uri": "https://pypi.org/project/pydantic-settings" + }, + { + "name": "pydash", + "uri": "https://pypi.org/project/pydash" + }, + { + "name": "pydata-google-auth", + "uri": "https://pypi.org/project/pydata-google-auth" + }, + { + "name": "pydata-sphinx-theme", + "uri": "https://pypi.org/project/pydata-sphinx-theme" + }, + { + "name": "pydeck", + "uri": "https://pypi.org/project/pydeck" + }, + { + "name": "pydeequ", + "uri": "https://pypi.org/project/pydeequ" + }, + { + "name": "pydevd", + "uri": "https://pypi.org/project/pydevd" + }, + { + "name": "pydicom", + "uri": "https://pypi.org/project/pydicom" + }, + { + "name": "pydispatcher", + "uri": "https://pypi.org/project/pydispatcher" + }, + { + "name": "pydocstyle", + "uri": "https://pypi.org/project/pydocstyle" + }, + { + "name": "pydot", + "uri": "https://pypi.org/project/pydot" + }, + { + "name": "pydriller", + "uri": "https://pypi.org/project/pydriller" + }, + { + "name": "pydruid", + "uri": "https://pypi.org/project/pydruid" + }, + { + "name": "pydub", + "uri": "https://pypi.org/project/pydub" + }, + { + "name": "pydyf", + "uri": "https://pypi.org/project/pydyf" + }, + { + "name": "pyee", + "uri": "https://pypi.org/project/pyee" + }, + { + "name": "pyelftools", + "uri": "https://pypi.org/project/pyelftools" + }, + { + "name": "pyerfa", + "uri": "https://pypi.org/project/pyerfa" + }, + { + "name": "pyfakefs", + "uri": "https://pypi.org/project/pyfakefs" + }, + { + "name": "pyfiglet", + "uri": "https://pypi.org/project/pyfiglet" + }, + { + "name": "pyflakes", + "uri": "https://pypi.org/project/pyflakes" + }, + { + "name": "pyformance", + "uri": "https://pypi.org/project/pyformance" + }, + { + "name": "pygame", + "uri": "https://pypi.org/project/pygame" + }, + { + "name": "pygeohash", + "uri": "https://pypi.org/project/pygeohash" + }, + { + "name": "pygetwindow", + "uri": "https://pypi.org/project/pygetwindow" + }, + { + "name": "pygit2", + "uri": "https://pypi.org/project/pygit2" + }, + { + "name": "pygithub", + "uri": "https://pypi.org/project/pygithub" + }, + { + "name": "pyglet", + "uri": "https://pypi.org/project/pyglet" + }, + { + "name": "pygments", + "uri": "https://pypi.org/project/pygments" + }, + { + "name": "pygobject", + "uri": "https://pypi.org/project/pygobject" + }, + { + "name": "pygsheets", + "uri": "https://pypi.org/project/pygsheets" + }, + { + "name": "pygtrie", + "uri": "https://pypi.org/project/pygtrie" + }, + { + "name": "pyhamcrest", + "uri": "https://pypi.org/project/pyhamcrest" + }, + { + "name": "pyhanko", + "uri": "https://pypi.org/project/pyhanko" + }, + { + "name": "pyhanko-certvalidator", + "uri": "https://pypi.org/project/pyhanko-certvalidator" + }, + { + "name": "pyhcl", + "uri": "https://pypi.org/project/pyhcl" + }, + { + "name": "pyhive", + "uri": "https://pypi.org/project/pyhive" + }, + { + "name": "pyhocon", + "uri": "https://pypi.org/project/pyhocon" + }, + { + "name": "pyhumps", + "uri": "https://pypi.org/project/pyhumps" + }, + { + "name": "pyiceberg", + "uri": "https://pypi.org/project/pyiceberg" + }, + { + "name": "pyinstaller", + "uri": "https://pypi.org/project/pyinstaller" + }, + { + "name": "pyinstaller-hooks-contrib", + "uri": "https://pypi.org/project/pyinstaller-hooks-contrib" + }, + { + "name": "pyinstrument", + "uri": "https://pypi.org/project/pyinstrument" + }, + { + "name": "pyjarowinkler", + "uri": "https://pypi.org/project/pyjarowinkler" + }, + { + "name": "pyjsparser", + "uri": "https://pypi.org/project/pyjsparser" + }, + { + "name": "pyjwt", + "uri": "https://pypi.org/project/pyjwt" + }, + { + "name": "pykakasi", + "uri": "https://pypi.org/project/pykakasi" + }, + { + "name": "pykwalify", + "uri": "https://pypi.org/project/pykwalify" + }, + { + "name": "pylev", + "uri": "https://pypi.org/project/pylev" + }, + { + "name": "pylint", + "uri": "https://pypi.org/project/pylint" + }, + { + "name": "pylint-django", + "uri": "https://pypi.org/project/pylint-django" + }, + { + "name": "pylint-plugin-utils", + "uri": "https://pypi.org/project/pylint-plugin-utils" + }, + { + "name": "pylru", + "uri": "https://pypi.org/project/pylru" + }, + { + "name": "pyluach", + "uri": "https://pypi.org/project/pyluach" + }, + { + "name": "pymatting", + "uri": "https://pypi.org/project/pymatting" + }, + { + "name": "pymdown-extensions", + "uri": "https://pypi.org/project/pymdown-extensions" + }, + { + "name": "pymeeus", + "uri": "https://pypi.org/project/pymeeus" + }, + { + "name": "pymemcache", + "uri": "https://pypi.org/project/pymemcache" + }, + { + "name": "pymilvus", + "uri": "https://pypi.org/project/pymilvus" + }, + { + "name": "pyminizip", + "uri": "https://pypi.org/project/pyminizip" + }, + { + "name": "pymisp", + "uri": "https://pypi.org/project/pymisp" + }, + { + "name": "pymongo", + "uri": "https://pypi.org/project/pymongo" + }, + { + "name": "pymongo-auth-aws", + "uri": "https://pypi.org/project/pymongo-auth-aws" + }, + { + "name": "pympler", + "uri": "https://pypi.org/project/pympler" + }, + { + "name": "pymsgbox", + "uri": "https://pypi.org/project/pymsgbox" + }, + { + "name": "pymssql", + "uri": "https://pypi.org/project/pymssql" + }, + { + "name": "pymsteams", + "uri": "https://pypi.org/project/pymsteams" + }, + { + "name": "pymupdf", + "uri": "https://pypi.org/project/pymupdf" + }, + { + "name": "pymupdfb", + "uri": "https://pypi.org/project/pymupdfb" + }, + { + "name": "pymysql", + "uri": "https://pypi.org/project/pymysql" + }, + { + "name": "pynacl", + "uri": "https://pypi.org/project/pynacl" + }, + { + "name": "pynamodb", + "uri": "https://pypi.org/project/pynamodb" + }, + { + "name": "pynetbox", + "uri": "https://pypi.org/project/pynetbox" + }, + { + "name": "pynndescent", + "uri": "https://pypi.org/project/pynndescent" + }, + { + "name": "pynput-robocorp-fork", + "uri": "https://pypi.org/project/pynput-robocorp-fork" + }, + { + "name": "pynvim", + "uri": "https://pypi.org/project/pynvim" + }, + { + "name": "pynvml", + "uri": "https://pypi.org/project/pynvml" + }, + { + "name": "pyod", + "uri": "https://pypi.org/project/pyod" + }, + { + "name": "pyodbc", + "uri": "https://pypi.org/project/pyodbc" + }, + { + "name": "pyogrio", + "uri": "https://pypi.org/project/pyogrio" + }, + { + "name": "pyopengl", + "uri": "https://pypi.org/project/pyopengl" + }, + { + "name": "pyopenssl", + "uri": "https://pypi.org/project/pyopenssl" + }, + { + "name": "pyorc", + "uri": "https://pypi.org/project/pyorc" + }, + { + "name": "pyotp", + "uri": "https://pypi.org/project/pyotp" + }, + { + "name": "pypandoc", + "uri": "https://pypi.org/project/pypandoc" + }, + { + "name": "pyparsing", + "uri": "https://pypi.org/project/pyparsing" + }, + { + "name": "pypdf", + "uri": "https://pypi.org/project/pypdf" + }, + { + "name": "pypdf2", + "uri": "https://pypi.org/project/pypdf2" + }, + { + "name": "pypdfium2", + "uri": "https://pypi.org/project/pypdfium2" + }, + { + "name": "pyperclip", + "uri": "https://pypi.org/project/pyperclip" + }, + { + "name": "pyphen", + "uri": "https://pypi.org/project/pyphen" + }, + { + "name": "pypika", + "uri": "https://pypi.org/project/pypika" + }, + { + "name": "pypinyin", + "uri": "https://pypi.org/project/pypinyin" + }, + { + "name": "pypiwin32", + "uri": "https://pypi.org/project/pypiwin32" + }, + { + "name": "pypng", + "uri": "https://pypi.org/project/pypng" + }, + { + "name": "pyppeteer", + "uri": "https://pypi.org/project/pyppeteer" + }, + { + "name": "pyppmd", + "uri": "https://pypi.org/project/pyppmd" + }, + { + "name": "pyproj", + "uri": "https://pypi.org/project/pyproj" + }, + { + "name": "pyproject-api", + "uri": "https://pypi.org/project/pyproject-api" + }, + { + "name": "pyproject-hooks", + "uri": "https://pypi.org/project/pyproject-hooks" + }, + { + "name": "pyproject-metadata", + "uri": "https://pypi.org/project/pyproject-metadata" + }, + { + "name": "pypyp", + "uri": "https://pypi.org/project/pypyp" + }, + { + "name": "pyqt5", + "uri": "https://pypi.org/project/pyqt5" + }, + { + "name": "pyqt5-qt5", + "uri": "https://pypi.org/project/pyqt5-qt5" + }, + { + "name": "pyqt5-sip", + "uri": "https://pypi.org/project/pyqt5-sip" + }, + { + "name": "pyqt6", + "uri": "https://pypi.org/project/pyqt6" + }, + { + "name": "pyqt6-qt6", + "uri": "https://pypi.org/project/pyqt6-qt6" + }, + { + "name": "pyqt6-sip", + "uri": "https://pypi.org/project/pyqt6-sip" + }, + { + "name": "pyquaternion", + "uri": "https://pypi.org/project/pyquaternion" + }, + { + "name": "pyquery", + "uri": "https://pypi.org/project/pyquery" + }, + { + "name": "pyramid", + "uri": "https://pypi.org/project/pyramid" + }, + { + "name": "pyrate-limiter", + "uri": "https://pypi.org/project/pyrate-limiter" + }, + { + "name": "pyreadline3", + "uri": "https://pypi.org/project/pyreadline3" + }, + { + "name": "pyrect", + "uri": "https://pypi.org/project/pyrect" + }, + { + "name": "pyrfc3339", + "uri": "https://pypi.org/project/pyrfc3339" + }, + { + "name": "pyright", + "uri": "https://pypi.org/project/pyright" + }, + { + "name": "pyroaring", + "uri": "https://pypi.org/project/pyroaring" + }, + { + "name": "pyrsistent", + "uri": "https://pypi.org/project/pyrsistent" + }, + { + "name": "pyrtf3", + "uri": "https://pypi.org/project/pyrtf3" + }, + { + "name": "pysaml2", + "uri": "https://pypi.org/project/pysaml2" + }, + { + "name": "pysbd", + "uri": "https://pypi.org/project/pysbd" + }, + { + "name": "pyscaffold", + "uri": "https://pypi.org/project/pyscaffold" + }, + { + "name": "pyscreeze", + "uri": "https://pypi.org/project/pyscreeze" + }, + { + "name": "pyserial", + "uri": "https://pypi.org/project/pyserial" + }, + { + "name": "pyserial-asyncio", + "uri": "https://pypi.org/project/pyserial-asyncio" + }, + { + "name": "pysftp", + "uri": "https://pypi.org/project/pysftp" + }, + { + "name": "pyshp", + "uri": "https://pypi.org/project/pyshp" + }, + { + "name": "pyside6", + "uri": "https://pypi.org/project/pyside6" + }, + { + "name": "pyside6-addons", + "uri": "https://pypi.org/project/pyside6-addons" + }, + { + "name": "pyside6-essentials", + "uri": "https://pypi.org/project/pyside6-essentials" + }, + { + "name": "pysmb", + "uri": "https://pypi.org/project/pysmb" + }, + { + "name": "pysmi", + "uri": "https://pypi.org/project/pysmi" + }, + { + "name": "pysnmp", + "uri": "https://pypi.org/project/pysnmp" + }, + { + "name": "pysocks", + "uri": "https://pypi.org/project/pysocks" + }, + { + "name": "pyspark", + "uri": "https://pypi.org/project/pyspark" + }, + { + "name": "pyspark-dist-explore", + "uri": "https://pypi.org/project/pyspark-dist-explore" + }, + { + "name": "pyspellchecker", + "uri": "https://pypi.org/project/pyspellchecker" + }, + { + "name": "pyspnego", + "uri": "https://pypi.org/project/pyspnego" + }, + { + "name": "pystache", + "uri": "https://pypi.org/project/pystache" + }, + { + "name": "pystan", + "uri": "https://pypi.org/project/pystan" + }, + { + "name": "pyston", + "uri": "https://pypi.org/project/pyston" + }, + { + "name": "pyston-autoload", + "uri": "https://pypi.org/project/pyston-autoload" + }, + { + "name": "pytablewriter", + "uri": "https://pypi.org/project/pytablewriter" + }, + { + "name": "pytd", + "uri": "https://pypi.org/project/pytd" + }, + { + "name": "pytelegrambotapi", + "uri": "https://pypi.org/project/pytelegrambotapi" + }, + { + "name": "pytesseract", + "uri": "https://pypi.org/project/pytesseract" + }, + { + "name": "pytest", + "uri": "https://pypi.org/project/pytest" + }, + { + "name": "pytest-aiohttp", + "uri": "https://pypi.org/project/pytest-aiohttp" + }, + { + "name": "pytest-alembic", + "uri": "https://pypi.org/project/pytest-alembic" + }, + { + "name": "pytest-ansible", + "uri": "https://pypi.org/project/pytest-ansible" + }, + { + "name": "pytest-assume", + "uri": "https://pypi.org/project/pytest-assume" + }, + { + "name": "pytest-asyncio", + "uri": "https://pypi.org/project/pytest-asyncio" + }, + { + "name": "pytest-azurepipelines", + "uri": "https://pypi.org/project/pytest-azurepipelines" + }, + { + "name": "pytest-base-url", + "uri": "https://pypi.org/project/pytest-base-url" + }, + { + "name": "pytest-bdd", + "uri": "https://pypi.org/project/pytest-bdd" + }, + { + "name": "pytest-benchmark", + "uri": "https://pypi.org/project/pytest-benchmark" + }, + { + "name": "pytest-check", + "uri": "https://pypi.org/project/pytest-check" + }, + { + "name": "pytest-cov", + "uri": "https://pypi.org/project/pytest-cov" + }, + { + "name": "pytest-custom-exit-code", + "uri": "https://pypi.org/project/pytest-custom-exit-code" + }, + { + "name": "pytest-dependency", + "uri": "https://pypi.org/project/pytest-dependency" + }, + { + "name": "pytest-django", + "uri": "https://pypi.org/project/pytest-django" + }, + { + "name": "pytest-dotenv", + "uri": "https://pypi.org/project/pytest-dotenv" + }, + { + "name": "pytest-env", + "uri": "https://pypi.org/project/pytest-env" + }, + { + "name": "pytest-flask", + "uri": "https://pypi.org/project/pytest-flask" + }, + { + "name": "pytest-forked", + "uri": "https://pypi.org/project/pytest-forked" + }, + { + "name": "pytest-freezegun", + "uri": "https://pypi.org/project/pytest-freezegun" + }, + { + "name": "pytest-html", + "uri": "https://pypi.org/project/pytest-html" + }, + { + "name": "pytest-httpserver", + "uri": "https://pypi.org/project/pytest-httpserver" + }, + { + "name": "pytest-httpx", + "uri": "https://pypi.org/project/pytest-httpx" + }, + { + "name": "pytest-icdiff", + "uri": "https://pypi.org/project/pytest-icdiff" + }, + { + "name": "pytest-instafail", + "uri": "https://pypi.org/project/pytest-instafail" + }, + { + "name": "pytest-json-report", + "uri": "https://pypi.org/project/pytest-json-report" + }, + { + "name": "pytest-localserver", + "uri": "https://pypi.org/project/pytest-localserver" + }, + { + "name": "pytest-messenger", + "uri": "https://pypi.org/project/pytest-messenger" + }, + { + "name": "pytest-metadata", + "uri": "https://pypi.org/project/pytest-metadata" + }, + { + "name": "pytest-mock", + "uri": "https://pypi.org/project/pytest-mock" + }, + { + "name": "pytest-mypy", + "uri": "https://pypi.org/project/pytest-mypy" + }, + { + "name": "pytest-order", + "uri": "https://pypi.org/project/pytest-order" + }, + { + "name": "pytest-ordering", + "uri": "https://pypi.org/project/pytest-ordering" + }, + { + "name": "pytest-parallel", + "uri": "https://pypi.org/project/pytest-parallel" + }, + { + "name": "pytest-playwright", + "uri": "https://pypi.org/project/pytest-playwright" + }, + { + "name": "pytest-random-order", + "uri": "https://pypi.org/project/pytest-random-order" + }, + { + "name": "pytest-randomly", + "uri": "https://pypi.org/project/pytest-randomly" + }, + { + "name": "pytest-repeat", + "uri": "https://pypi.org/project/pytest-repeat" + }, + { + "name": "pytest-rerunfailures", + "uri": "https://pypi.org/project/pytest-rerunfailures" + }, + { + "name": "pytest-runner", + "uri": "https://pypi.org/project/pytest-runner" + }, + { + "name": "pytest-socket", + "uri": "https://pypi.org/project/pytest-socket" + }, + { + "name": "pytest-split", + "uri": "https://pypi.org/project/pytest-split" + }, + { + "name": "pytest-subtests", + "uri": "https://pypi.org/project/pytest-subtests" + }, + { + "name": "pytest-sugar", + "uri": "https://pypi.org/project/pytest-sugar" + }, + { + "name": "pytest-timeout", + "uri": "https://pypi.org/project/pytest-timeout" + }, + { + "name": "pytest-xdist", + "uri": "https://pypi.org/project/pytest-xdist" + }, + { + "name": "python-arango", + "uri": "https://pypi.org/project/python-arango" + }, + { + "name": "python-bidi", + "uri": "https://pypi.org/project/python-bidi" + }, + { + "name": "python-box", + "uri": "https://pypi.org/project/python-box" + }, + { + "name": "python-can", + "uri": "https://pypi.org/project/python-can" + }, + { + "name": "python-certifi-win32", + "uri": "https://pypi.org/project/python-certifi-win32" + }, + { + "name": "python-consul", + "uri": "https://pypi.org/project/python-consul" + }, + { + "name": "python-crfsuite", + "uri": "https://pypi.org/project/python-crfsuite" + }, + { + "name": "python-crontab", + "uri": "https://pypi.org/project/python-crontab" + }, + { + "name": "python-daemon", + "uri": "https://pypi.org/project/python-daemon" + }, + { + "name": "python-dateutil", + "uri": "https://pypi.org/project/python-dateutil" + }, + { + "name": "python-decouple", + "uri": "https://pypi.org/project/python-decouple" + }, + { + "name": "python-docx", + "uri": "https://pypi.org/project/python-docx" + }, + { + "name": "python-dotenv", + "uri": "https://pypi.org/project/python-dotenv" + }, + { + "name": "python-editor", + "uri": "https://pypi.org/project/python-editor" + }, + { + "name": "python-engineio", + "uri": "https://pypi.org/project/python-engineio" + }, + { + "name": "python-gettext", + "uri": "https://pypi.org/project/python-gettext" + }, + { + "name": "python-gitlab", + "uri": "https://pypi.org/project/python-gitlab" + }, + { + "name": "python-gnupg", + "uri": "https://pypi.org/project/python-gnupg" + }, + { + "name": "python-hcl2", + "uri": "https://pypi.org/project/python-hcl2" + }, + { + "name": "python-http-client", + "uri": "https://pypi.org/project/python-http-client" + }, + { + "name": "python-igraph", + "uri": "https://pypi.org/project/python-igraph" + }, + { + "name": "python-ipware", + "uri": "https://pypi.org/project/python-ipware" + }, + { + "name": "python-iso639", + "uri": "https://pypi.org/project/python-iso639" + }, + { + "name": "python-jenkins", + "uri": "https://pypi.org/project/python-jenkins" + }, + { + "name": "python-jose", + "uri": "https://pypi.org/project/python-jose" + }, + { + "name": "python-json-logger", + "uri": "https://pypi.org/project/python-json-logger" + }, + { + "name": "python-keycloak", + "uri": "https://pypi.org/project/python-keycloak" + }, + { + "name": "python-keystoneclient", + "uri": "https://pypi.org/project/python-keystoneclient" + }, + { + "name": "python-ldap", + "uri": "https://pypi.org/project/python-ldap" + }, + { + "name": "python-levenshtein", + "uri": "https://pypi.org/project/python-levenshtein" + }, + { + "name": "python-logging-loki", + "uri": "https://pypi.org/project/python-logging-loki" + }, + { + "name": "python-lsp-jsonrpc", + "uri": "https://pypi.org/project/python-lsp-jsonrpc" + }, + { + "name": "python-magic", + "uri": "https://pypi.org/project/python-magic" + }, + { + "name": "python-memcached", + "uri": "https://pypi.org/project/python-memcached" + }, + { + "name": "python-miio", + "uri": "https://pypi.org/project/python-miio" + }, + { + "name": "python-multipart", + "uri": "https://pypi.org/project/python-multipart" + }, + { + "name": "python-nvd3", + "uri": "https://pypi.org/project/python-nvd3" + }, + { + "name": "python-on-whales", + "uri": "https://pypi.org/project/python-on-whales" + }, + { + "name": "python-pam", + "uri": "https://pypi.org/project/python-pam" + }, + { + "name": "python-pptx", + "uri": "https://pypi.org/project/python-pptx" + }, + { + "name": "python-rapidjson", + "uri": "https://pypi.org/project/python-rapidjson" + }, + { + "name": "python-slugify", + "uri": "https://pypi.org/project/python-slugify" + }, + { + "name": "python-snappy", + "uri": "https://pypi.org/project/python-snappy" + }, + { + "name": "python-socketio", + "uri": "https://pypi.org/project/python-socketio" + }, + { + "name": "python-stdnum", + "uri": "https://pypi.org/project/python-stdnum" + }, + { + "name": "python-string-utils", + "uri": "https://pypi.org/project/python-string-utils" + }, + { + "name": "python-telegram-bot", + "uri": "https://pypi.org/project/python-telegram-bot" + }, + { + "name": "python-ulid", + "uri": "https://pypi.org/project/python-ulid" + }, + { + "name": "python-utils", + "uri": "https://pypi.org/project/python-utils" + }, + { + "name": "python-xlib", + "uri": "https://pypi.org/project/python-xlib" + }, + { + "name": "python3-logstash", + "uri": "https://pypi.org/project/python3-logstash" + }, + { + "name": "python3-openid", + "uri": "https://pypi.org/project/python3-openid" + }, + { + "name": "python3-saml", + "uri": "https://pypi.org/project/python3-saml" + }, + { + "name": "pythonnet", + "uri": "https://pypi.org/project/pythonnet" + }, + { + "name": "pythran-openblas", + "uri": "https://pypi.org/project/pythran-openblas" + }, + { + "name": "pytimeparse", + "uri": "https://pypi.org/project/pytimeparse" + }, + { + "name": "pytimeparse2", + "uri": "https://pypi.org/project/pytimeparse2" + }, + { + "name": "pytoolconfig", + "uri": "https://pypi.org/project/pytoolconfig" + }, + { + "name": "pytorch-lightning", + "uri": "https://pypi.org/project/pytorch-lightning" + }, + { + "name": "pytorch-metric-learning", + "uri": "https://pypi.org/project/pytorch-metric-learning" + }, + { + "name": "pytube", + "uri": "https://pypi.org/project/pytube" + }, + { + "name": "pytweening", + "uri": "https://pypi.org/project/pytweening" + }, + { + "name": "pytz", + "uri": "https://pypi.org/project/pytz" + }, + { + "name": "pytz-deprecation-shim", + "uri": "https://pypi.org/project/pytz-deprecation-shim" + }, + { + "name": "pytzdata", + "uri": "https://pypi.org/project/pytzdata" + }, + { + "name": "pyu2f", + "uri": "https://pypi.org/project/pyu2f" + }, + { + "name": "pyudev", + "uri": "https://pypi.org/project/pyudev" + }, + { + "name": "pyunormalize", + "uri": "https://pypi.org/project/pyunormalize" + }, + { + "name": "pyusb", + "uri": "https://pypi.org/project/pyusb" + }, + { + "name": "pyvinecopulib", + "uri": "https://pypi.org/project/pyvinecopulib" + }, + { + "name": "pyvirtualdisplay", + "uri": "https://pypi.org/project/pyvirtualdisplay" + }, + { + "name": "pyvis", + "uri": "https://pypi.org/project/pyvis" + }, + { + "name": "pyvisa", + "uri": "https://pypi.org/project/pyvisa" + }, + { + "name": "pyviz-comms", + "uri": "https://pypi.org/project/pyviz-comms" + }, + { + "name": "pyvmomi", + "uri": "https://pypi.org/project/pyvmomi" + }, + { + "name": "pywavelets", + "uri": "https://pypi.org/project/pywavelets" + }, + { + "name": "pywin32", + "uri": "https://pypi.org/project/pywin32" + }, + { + "name": "pywin32-ctypes", + "uri": "https://pypi.org/project/pywin32-ctypes" + }, + { + "name": "pywinauto", + "uri": "https://pypi.org/project/pywinauto" + }, + { + "name": "pywinpty", + "uri": "https://pypi.org/project/pywinpty" + }, + { + "name": "pywinrm", + "uri": "https://pypi.org/project/pywinrm" + }, + { + "name": "pyxdg", + "uri": "https://pypi.org/project/pyxdg" + }, + { + "name": "pyxlsb", + "uri": "https://pypi.org/project/pyxlsb" + }, + { + "name": "pyyaml", + "uri": "https://pypi.org/project/pyyaml" + }, + { + "name": "pyyaml-env-tag", + "uri": "https://pypi.org/project/pyyaml-env-tag" + }, + { + "name": "pyzipper", + "uri": "https://pypi.org/project/pyzipper" + }, + { + "name": "pyzmq", + "uri": "https://pypi.org/project/pyzmq" + }, + { + "name": "pyzstd", + "uri": "https://pypi.org/project/pyzstd" + }, + { + "name": "qdldl", + "uri": "https://pypi.org/project/qdldl" + }, + { + "name": "qdrant-client", + "uri": "https://pypi.org/project/qdrant-client" + }, + { + "name": "qiskit", + "uri": "https://pypi.org/project/qiskit" + }, + { + "name": "qrcode", + "uri": "https://pypi.org/project/qrcode" + }, + { + "name": "qtconsole", + "uri": "https://pypi.org/project/qtconsole" + }, + { + "name": "qtpy", + "uri": "https://pypi.org/project/qtpy" + }, + { + "name": "quantlib", + "uri": "https://pypi.org/project/quantlib" + }, + { + "name": "quart", + "uri": "https://pypi.org/project/quart" + }, + { + "name": "qudida", + "uri": "https://pypi.org/project/qudida" + }, + { + "name": "querystring-parser", + "uri": "https://pypi.org/project/querystring-parser" + }, + { + "name": "questionary", + "uri": "https://pypi.org/project/questionary" + }, + { + "name": "queuelib", + "uri": "https://pypi.org/project/queuelib" + }, + { + "name": "quinn", + "uri": "https://pypi.org/project/quinn" + }, + { + "name": "radon", + "uri": "https://pypi.org/project/radon" + }, + { + "name": "random-password-generator", + "uri": "https://pypi.org/project/random-password-generator" + }, + { + "name": "rangehttpserver", + "uri": "https://pypi.org/project/rangehttpserver" + }, + { + "name": "rapidfuzz", + "uri": "https://pypi.org/project/rapidfuzz" + }, + { + "name": "rasterio", + "uri": "https://pypi.org/project/rasterio" + }, + { + "name": "ratelim", + "uri": "https://pypi.org/project/ratelim" + }, + { + "name": "ratelimit", + "uri": "https://pypi.org/project/ratelimit" + }, + { + "name": "ratelimiter", + "uri": "https://pypi.org/project/ratelimiter" + }, + { + "name": "raven", + "uri": "https://pypi.org/project/raven" + }, + { + "name": "ray", + "uri": "https://pypi.org/project/ray" + }, + { + "name": "rcssmin", + "uri": "https://pypi.org/project/rcssmin" + }, + { + "name": "rdflib", + "uri": "https://pypi.org/project/rdflib" + }, + { + "name": "rdkit", + "uri": "https://pypi.org/project/rdkit" + }, + { + "name": "reactivex", + "uri": "https://pypi.org/project/reactivex" + }, + { + "name": "readchar", + "uri": "https://pypi.org/project/readchar" + }, + { + "name": "readme-renderer", + "uri": "https://pypi.org/project/readme-renderer" + }, + { + "name": "readthedocs-sphinx-ext", + "uri": "https://pypi.org/project/readthedocs-sphinx-ext" + }, + { + "name": "realtime", + "uri": "https://pypi.org/project/realtime" + }, + { + "name": "recommonmark", + "uri": "https://pypi.org/project/recommonmark" + }, + { + "name": "recordlinkage", + "uri": "https://pypi.org/project/recordlinkage" + }, + { + "name": "red-discordbot", + "uri": "https://pypi.org/project/red-discordbot" + }, + { + "name": "redis", + "uri": "https://pypi.org/project/redis" + }, + { + "name": "redis-py-cluster", + "uri": "https://pypi.org/project/redis-py-cluster" + }, + { + "name": "redshift-connector", + "uri": "https://pypi.org/project/redshift-connector" + }, + { + "name": "referencing", + "uri": "https://pypi.org/project/referencing" + }, + { + "name": "regex", + "uri": "https://pypi.org/project/regex" + }, + { + "name": "regress", + "uri": "https://pypi.org/project/regress" + }, + { + "name": "rembg", + "uri": "https://pypi.org/project/rembg" + }, + { + "name": "reportlab", + "uri": "https://pypi.org/project/reportlab" + }, + { + "name": "repoze-lru", + "uri": "https://pypi.org/project/repoze-lru" + }, + { + "name": "requests", + "uri": "https://pypi.org/project/requests" + }, + { + "name": "requests-auth-aws-sigv4", + "uri": "https://pypi.org/project/requests-auth-aws-sigv4" + }, + { + "name": "requests-aws-sign", + "uri": "https://pypi.org/project/requests-aws-sign" + }, + { + "name": "requests-aws4auth", + "uri": "https://pypi.org/project/requests-aws4auth" + }, + { + "name": "requests-cache", + "uri": "https://pypi.org/project/requests-cache" + }, + { + "name": "requests-file", + "uri": "https://pypi.org/project/requests-file" + }, + { + "name": "requests-futures", + "uri": "https://pypi.org/project/requests-futures" + }, + { + "name": "requests-html", + "uri": "https://pypi.org/project/requests-html" + }, + { + "name": "requests-mock", + "uri": "https://pypi.org/project/requests-mock" + }, + { + "name": "requests-ntlm", + "uri": "https://pypi.org/project/requests-ntlm" + }, + { + "name": "requests-oauthlib", + "uri": "https://pypi.org/project/requests-oauthlib" + }, + { + "name": "requests-pkcs12", + "uri": "https://pypi.org/project/requests-pkcs12" + }, + { + "name": "requests-sigv4", + "uri": "https://pypi.org/project/requests-sigv4" + }, + { + "name": "requests-toolbelt", + "uri": "https://pypi.org/project/requests-toolbelt" + }, + { + "name": "requests-unixsocket", + "uri": "https://pypi.org/project/requests-unixsocket" + }, + { + "name": "requestsexceptions", + "uri": "https://pypi.org/project/requestsexceptions" + }, + { + "name": "requirements-parser", + "uri": "https://pypi.org/project/requirements-parser" + }, + { + "name": "resampy", + "uri": "https://pypi.org/project/resampy" + }, + { + "name": "resize-right", + "uri": "https://pypi.org/project/resize-right" + }, + { + "name": "resolvelib", + "uri": "https://pypi.org/project/resolvelib" + }, + { + "name": "responses", + "uri": "https://pypi.org/project/responses" + }, + { + "name": "respx", + "uri": "https://pypi.org/project/respx" + }, + { + "name": "restrictedpython", + "uri": "https://pypi.org/project/restrictedpython" + }, + { + "name": "result", + "uri": "https://pypi.org/project/result" + }, + { + "name": "retry", + "uri": "https://pypi.org/project/retry" + }, + { + "name": "retry-decorator", + "uri": "https://pypi.org/project/retry-decorator" + }, + { + "name": "retry2", + "uri": "https://pypi.org/project/retry2" + }, + { + "name": "retrying", + "uri": "https://pypi.org/project/retrying" + }, + { + "name": "rfc3339", + "uri": "https://pypi.org/project/rfc3339" + }, + { + "name": "rfc3339-validator", + "uri": "https://pypi.org/project/rfc3339-validator" + }, + { + "name": "rfc3986", + "uri": "https://pypi.org/project/rfc3986" + }, + { + "name": "rfc3986-validator", + "uri": "https://pypi.org/project/rfc3986-validator" + }, + { + "name": "rfc3987", + "uri": "https://pypi.org/project/rfc3987" + }, + { + "name": "rich", + "uri": "https://pypi.org/project/rich" + }, + { + "name": "rich-argparse", + "uri": "https://pypi.org/project/rich-argparse" + }, + { + "name": "rich-click", + "uri": "https://pypi.org/project/rich-click" + }, + { + "name": "riot", + "uri": "https://pypi.org/project/riot" + }, + { + "name": "rjsmin", + "uri": "https://pypi.org/project/rjsmin" + }, + { + "name": "rlp", + "uri": "https://pypi.org/project/rlp" + }, + { + "name": "rmsd", + "uri": "https://pypi.org/project/rmsd" + }, + { + "name": "robocorp-storage", + "uri": "https://pypi.org/project/robocorp-storage" + }, + { + "name": "robotframework", + "uri": "https://pypi.org/project/robotframework" + }, + { + "name": "robotframework-pythonlibcore", + "uri": "https://pypi.org/project/robotframework-pythonlibcore" + }, + { + "name": "robotframework-requests", + "uri": "https://pypi.org/project/robotframework-requests" + }, + { + "name": "robotframework-seleniumlibrary", + "uri": "https://pypi.org/project/robotframework-seleniumlibrary" + }, + { + "name": "robotframework-seleniumtestability", + "uri": "https://pypi.org/project/robotframework-seleniumtestability" + }, + { + "name": "rollbar", + "uri": "https://pypi.org/project/rollbar" + }, + { + "name": "roman", + "uri": "https://pypi.org/project/roman" + }, + { + "name": "rope", + "uri": "https://pypi.org/project/rope" + }, + { + "name": "rouge-score", + "uri": "https://pypi.org/project/rouge-score" + }, + { + "name": "routes", + "uri": "https://pypi.org/project/routes" + }, + { + "name": "rpaframework", + "uri": "https://pypi.org/project/rpaframework" + }, + { + "name": "rpaframework-core", + "uri": "https://pypi.org/project/rpaframework-core" + }, + { + "name": "rpaframework-pdf", + "uri": "https://pypi.org/project/rpaframework-pdf" + }, + { + "name": "rpds-py", + "uri": "https://pypi.org/project/rpds-py" + }, + { + "name": "rply", + "uri": "https://pypi.org/project/rply" + }, + { + "name": "rpyc", + "uri": "https://pypi.org/project/rpyc" + }, + { + "name": "rq", + "uri": "https://pypi.org/project/rq" + }, + { + "name": "rsa", + "uri": "https://pypi.org/project/rsa" + }, + { + "name": "rstr", + "uri": "https://pypi.org/project/rstr" + }, + { + "name": "rtree", + "uri": "https://pypi.org/project/rtree" + }, + { + "name": "ruamel-yaml", + "uri": "https://pypi.org/project/ruamel-yaml" + }, + { + "name": "ruamel-yaml-clib", + "uri": "https://pypi.org/project/ruamel-yaml-clib" + }, + { + "name": "ruff", + "uri": "https://pypi.org/project/ruff" + }, + { + "name": "runs", + "uri": "https://pypi.org/project/runs" + }, + { + "name": "ruptures", + "uri": "https://pypi.org/project/ruptures" + }, + { + "name": "rustworkx", + "uri": "https://pypi.org/project/rustworkx" + }, + { + "name": "ruyaml", + "uri": "https://pypi.org/project/ruyaml" + }, + { + "name": "rx", + "uri": "https://pypi.org/project/rx" + }, + { + "name": "s3cmd", + "uri": "https://pypi.org/project/s3cmd" + }, + { + "name": "s3fs", + "uri": "https://pypi.org/project/s3fs" + }, + { + "name": "s3path", + "uri": "https://pypi.org/project/s3path" + }, + { + "name": "s3transfer", + "uri": "https://pypi.org/project/s3transfer" + }, + { + "name": "sacrebleu", + "uri": "https://pypi.org/project/sacrebleu" + }, + { + "name": "sacremoses", + "uri": "https://pypi.org/project/sacremoses" + }, + { + "name": "safetensors", + "uri": "https://pypi.org/project/safetensors" + }, + { + "name": "safety", + "uri": "https://pypi.org/project/safety" + }, + { + "name": "safety-schemas", + "uri": "https://pypi.org/project/safety-schemas" + }, + { + "name": "sagemaker", + "uri": "https://pypi.org/project/sagemaker" + }, + { + "name": "sagemaker-core", + "uri": "https://pypi.org/project/sagemaker-core" + }, + { + "name": "sagemaker-mlflow", + "uri": "https://pypi.org/project/sagemaker-mlflow" + }, + { + "name": "salesforce-bulk", + "uri": "https://pypi.org/project/salesforce-bulk" + }, + { + "name": "sampleproject", + "uri": "https://pypi.org/project/sampleproject" + }, + { + "name": "sanic", + "uri": "https://pypi.org/project/sanic" + }, + { + "name": "sanic-routing", + "uri": "https://pypi.org/project/sanic-routing" + }, + { + "name": "sarif-om", + "uri": "https://pypi.org/project/sarif-om" + }, + { + "name": "sasl", + "uri": "https://pypi.org/project/sasl" + }, + { + "name": "scandir", + "uri": "https://pypi.org/project/scandir" + }, + { + "name": "scapy", + "uri": "https://pypi.org/project/scapy" + }, + { + "name": "schedule", + "uri": "https://pypi.org/project/schedule" + }, + { + "name": "schema", + "uri": "https://pypi.org/project/schema" + }, + { + "name": "schematics", + "uri": "https://pypi.org/project/schematics" + }, + { + "name": "schemdraw", + "uri": "https://pypi.org/project/schemdraw" + }, + { + "name": "scikit-build", + "uri": "https://pypi.org/project/scikit-build" + }, + { + "name": "scikit-build-core", + "uri": "https://pypi.org/project/scikit-build-core" + }, + { + "name": "scikit-image", + "uri": "https://pypi.org/project/scikit-image" + }, + { + "name": "scikit-learn", + "uri": "https://pypi.org/project/scikit-learn" + }, + { + "name": "scikit-optimize", + "uri": "https://pypi.org/project/scikit-optimize" + }, + { + "name": "scipy", + "uri": "https://pypi.org/project/scipy" + }, + { + "name": "scons", + "uri": "https://pypi.org/project/scons" + }, + { + "name": "scp", + "uri": "https://pypi.org/project/scp" + }, + { + "name": "scramp", + "uri": "https://pypi.org/project/scramp" + }, + { + "name": "scrapy", + "uri": "https://pypi.org/project/scrapy" + }, + { + "name": "scrypt", + "uri": "https://pypi.org/project/scrypt" + }, + { + "name": "scs", + "uri": "https://pypi.org/project/scs" + }, + { + "name": "seaborn", + "uri": "https://pypi.org/project/seaborn" + }, + { + "name": "secretstorage", + "uri": "https://pypi.org/project/secretstorage" + }, + { + "name": "segment-analytics-python", + "uri": "https://pypi.org/project/segment-analytics-python" + }, + { + "name": "segment-anything", + "uri": "https://pypi.org/project/segment-anything" + }, + { + "name": "selenium", + "uri": "https://pypi.org/project/selenium" + }, + { + "name": "selenium-wire", + "uri": "https://pypi.org/project/selenium-wire" + }, + { + "name": "seleniumbase", + "uri": "https://pypi.org/project/seleniumbase" + }, + { + "name": "semantic-version", + "uri": "https://pypi.org/project/semantic-version" + }, + { + "name": "semgrep", + "uri": "https://pypi.org/project/semgrep" + }, + { + "name": "semver", + "uri": "https://pypi.org/project/semver" + }, + { + "name": "send2trash", + "uri": "https://pypi.org/project/send2trash" + }, + { + "name": "sendgrid", + "uri": "https://pypi.org/project/sendgrid" + }, + { + "name": "sentence-transformers", + "uri": "https://pypi.org/project/sentence-transformers" + }, + { + "name": "sentencepiece", + "uri": "https://pypi.org/project/sentencepiece" + }, + { + "name": "sentinels", + "uri": "https://pypi.org/project/sentinels" + }, + { + "name": "sentry-sdk", + "uri": "https://pypi.org/project/sentry-sdk" + }, + { + "name": "seqio-nightly", + "uri": "https://pypi.org/project/seqio-nightly" + }, + { + "name": "serial", + "uri": "https://pypi.org/project/serial" + }, + { + "name": "service-identity", + "uri": "https://pypi.org/project/service-identity" + }, + { + "name": "setproctitle", + "uri": "https://pypi.org/project/setproctitle" + }, + { + "name": "setuptools", + "uri": "https://pypi.org/project/setuptools" + }, + { + "name": "setuptools-git", + "uri": "https://pypi.org/project/setuptools-git" + }, + { + "name": "setuptools-git-versioning", + "uri": "https://pypi.org/project/setuptools-git-versioning" + }, + { + "name": "setuptools-rust", + "uri": "https://pypi.org/project/setuptools-rust" + }, + { + "name": "setuptools-scm", + "uri": "https://pypi.org/project/setuptools-scm" + }, + { + "name": "setuptools-scm-git-archive", + "uri": "https://pypi.org/project/setuptools-scm-git-archive" + }, + { + "name": "sgmllib3k", + "uri": "https://pypi.org/project/sgmllib3k" + }, + { + "name": "sgp4", + "uri": "https://pypi.org/project/sgp4" + }, + { + "name": "sgqlc", + "uri": "https://pypi.org/project/sgqlc" + }, + { + "name": "sh", + "uri": "https://pypi.org/project/sh" + }, + { + "name": "shap", + "uri": "https://pypi.org/project/shap" + }, + { + "name": "shapely", + "uri": "https://pypi.org/project/shapely" + }, + { + "name": "shareplum", + "uri": "https://pypi.org/project/shareplum" + }, + { + "name": "sharepy", + "uri": "https://pypi.org/project/sharepy" + }, + { + "name": "shellescape", + "uri": "https://pypi.org/project/shellescape" + }, + { + "name": "shellingham", + "uri": "https://pypi.org/project/shellingham" + }, + { + "name": "shiboken6", + "uri": "https://pypi.org/project/shiboken6" + }, + { + "name": "shortuuid", + "uri": "https://pypi.org/project/shortuuid" + }, + { + "name": "shtab", + "uri": "https://pypi.org/project/shtab" + }, + { + "name": "shyaml", + "uri": "https://pypi.org/project/shyaml" + }, + { + "name": "signalfx", + "uri": "https://pypi.org/project/signalfx" + }, + { + "name": "signxml", + "uri": "https://pypi.org/project/signxml" + }, + { + "name": "silpa-common", + "uri": "https://pypi.org/project/silpa-common" + }, + { + "name": "simple-ddl-parser", + "uri": "https://pypi.org/project/simple-ddl-parser" + }, + { + "name": "simple-parsing", + "uri": "https://pypi.org/project/simple-parsing" + }, + { + "name": "simple-salesforce", + "uri": "https://pypi.org/project/simple-salesforce" + }, + { + "name": "simple-term-menu", + "uri": "https://pypi.org/project/simple-term-menu" + }, + { + "name": "simple-websocket", + "uri": "https://pypi.org/project/simple-websocket" + }, + { + "name": "simpleeval", + "uri": "https://pypi.org/project/simpleeval" + }, + { + "name": "simplegeneric", + "uri": "https://pypi.org/project/simplegeneric" + }, + { + "name": "simplejson", + "uri": "https://pypi.org/project/simplejson" + }, + { + "name": "simpy", + "uri": "https://pypi.org/project/simpy" + }, + { + "name": "singer-python", + "uri": "https://pypi.org/project/singer-python" + }, + { + "name": "singer-sdk", + "uri": "https://pypi.org/project/singer-sdk" + }, + { + "name": "singledispatch", + "uri": "https://pypi.org/project/singledispatch" + }, + { + "name": "singleton-decorator", + "uri": "https://pypi.org/project/singleton-decorator" + }, + { + "name": "six", + "uri": "https://pypi.org/project/six" + }, + { + "name": "skl2onnx", + "uri": "https://pypi.org/project/skl2onnx" + }, + { + "name": "sklearn", + "uri": "https://pypi.org/project/sklearn" + }, + { + "name": "sktime", + "uri": "https://pypi.org/project/sktime" + }, + { + "name": "skyfield", + "uri": "https://pypi.org/project/skyfield" + }, + { + "name": "slack-bolt", + "uri": "https://pypi.org/project/slack-bolt" + }, + { + "name": "slack-sdk", + "uri": "https://pypi.org/project/slack-sdk" + }, + { + "name": "slackclient", + "uri": "https://pypi.org/project/slackclient" + }, + { + "name": "slacker", + "uri": "https://pypi.org/project/slacker" + }, + { + "name": "slicer", + "uri": "https://pypi.org/project/slicer" + }, + { + "name": "slotted", + "uri": "https://pypi.org/project/slotted" + }, + { + "name": "smart-open", + "uri": "https://pypi.org/project/smart-open" + }, + { + "name": "smartsheet-python-sdk", + "uri": "https://pypi.org/project/smartsheet-python-sdk" + }, + { + "name": "smbprotocol", + "uri": "https://pypi.org/project/smbprotocol" + }, + { + "name": "smdebug-rulesconfig", + "uri": "https://pypi.org/project/smdebug-rulesconfig" + }, + { + "name": "smmap", + "uri": "https://pypi.org/project/smmap" + }, + { + "name": "smmap2", + "uri": "https://pypi.org/project/smmap2" + }, + { + "name": "sniffio", + "uri": "https://pypi.org/project/sniffio" + }, + { + "name": "snowballstemmer", + "uri": "https://pypi.org/project/snowballstemmer" + }, + { + "name": "snowflake", + "uri": "https://pypi.org/project/snowflake" + }, + { + "name": "snowflake-connector-python", + "uri": "https://pypi.org/project/snowflake-connector-python" + }, + { + "name": "snowflake-core", + "uri": "https://pypi.org/project/snowflake-core" + }, + { + "name": "snowflake-legacy", + "uri": "https://pypi.org/project/snowflake-legacy" + }, + { + "name": "snowflake-snowpark-python", + "uri": "https://pypi.org/project/snowflake-snowpark-python" + }, + { + "name": "snowflake-sqlalchemy", + "uri": "https://pypi.org/project/snowflake-sqlalchemy" + }, + { + "name": "snuggs", + "uri": "https://pypi.org/project/snuggs" + }, + { + "name": "social-auth-app-django", + "uri": "https://pypi.org/project/social-auth-app-django" + }, + { + "name": "social-auth-core", + "uri": "https://pypi.org/project/social-auth-core" + }, + { + "name": "socksio", + "uri": "https://pypi.org/project/socksio" + }, + { + "name": "soda-core", + "uri": "https://pypi.org/project/soda-core" + }, + { + "name": "soda-core-spark", + "uri": "https://pypi.org/project/soda-core-spark" + }, + { + "name": "soda-core-spark-df", + "uri": "https://pypi.org/project/soda-core-spark-df" + }, + { + "name": "sodapy", + "uri": "https://pypi.org/project/sodapy" + }, + { + "name": "sortedcontainers", + "uri": "https://pypi.org/project/sortedcontainers" + }, + { + "name": "sounddevice", + "uri": "https://pypi.org/project/sounddevice" + }, + { + "name": "soundex", + "uri": "https://pypi.org/project/soundex" + }, + { + "name": "soundfile", + "uri": "https://pypi.org/project/soundfile" + }, + { + "name": "soupsieve", + "uri": "https://pypi.org/project/soupsieve" + }, + { + "name": "soxr", + "uri": "https://pypi.org/project/soxr" + }, + { + "name": "spacy", + "uri": "https://pypi.org/project/spacy" + }, + { + "name": "spacy-legacy", + "uri": "https://pypi.org/project/spacy-legacy" + }, + { + "name": "spacy-loggers", + "uri": "https://pypi.org/project/spacy-loggers" + }, + { + "name": "spacy-transformers", + "uri": "https://pypi.org/project/spacy-transformers" + }, + { + "name": "spacy-wordnet", + "uri": "https://pypi.org/project/spacy-wordnet" + }, + { + "name": "spandrel", + "uri": "https://pypi.org/project/spandrel" + }, + { + "name": "spark-nlp", + "uri": "https://pypi.org/project/spark-nlp" + }, + { + "name": "spark-sklearn", + "uri": "https://pypi.org/project/spark-sklearn" + }, + { + "name": "sparkorm", + "uri": "https://pypi.org/project/sparkorm" + }, + { + "name": "sparqlwrapper", + "uri": "https://pypi.org/project/sparqlwrapper" + }, + { + "name": "spdx-tools", + "uri": "https://pypi.org/project/spdx-tools" + }, + { + "name": "speechbrain", + "uri": "https://pypi.org/project/speechbrain" + }, + { + "name": "speechrecognition", + "uri": "https://pypi.org/project/speechrecognition" + }, + { + "name": "spellchecker", + "uri": "https://pypi.org/project/spellchecker" + }, + { + "name": "sphinx", + "uri": "https://pypi.org/project/sphinx" + }, + { + "name": "sphinx-argparse", + "uri": "https://pypi.org/project/sphinx-argparse" + }, + { + "name": "sphinx-autobuild", + "uri": "https://pypi.org/project/sphinx-autobuild" + }, + { + "name": "sphinx-autodoc-typehints", + "uri": "https://pypi.org/project/sphinx-autodoc-typehints" + }, + { + "name": "sphinx-basic-ng", + "uri": "https://pypi.org/project/sphinx-basic-ng" + }, + { + "name": "sphinx-book-theme", + "uri": "https://pypi.org/project/sphinx-book-theme" + }, + { + "name": "sphinx-copybutton", + "uri": "https://pypi.org/project/sphinx-copybutton" + }, + { + "name": "sphinx-design", + "uri": "https://pypi.org/project/sphinx-design" + }, + { + "name": "sphinx-rtd-theme", + "uri": "https://pypi.org/project/sphinx-rtd-theme" + }, + { + "name": "sphinx-tabs", + "uri": "https://pypi.org/project/sphinx-tabs" + }, + { + "name": "sphinxcontrib-applehelp", + "uri": "https://pypi.org/project/sphinxcontrib-applehelp" + }, + { + "name": "sphinxcontrib-bibtex", + "uri": "https://pypi.org/project/sphinxcontrib-bibtex" + }, + { + "name": "sphinxcontrib-devhelp", + "uri": "https://pypi.org/project/sphinxcontrib-devhelp" + }, + { + "name": "sphinxcontrib-htmlhelp", + "uri": "https://pypi.org/project/sphinxcontrib-htmlhelp" + }, + { + "name": "sphinxcontrib-jquery", + "uri": "https://pypi.org/project/sphinxcontrib-jquery" + }, + { + "name": "sphinxcontrib-jsmath", + "uri": "https://pypi.org/project/sphinxcontrib-jsmath" + }, + { + "name": "sphinxcontrib-mermaid", + "uri": "https://pypi.org/project/sphinxcontrib-mermaid" + }, + { + "name": "sphinxcontrib-qthelp", + "uri": "https://pypi.org/project/sphinxcontrib-qthelp" + }, + { + "name": "sphinxcontrib-serializinghtml", + "uri": "https://pypi.org/project/sphinxcontrib-serializinghtml" + }, + { + "name": "sphinxcontrib-websupport", + "uri": "https://pypi.org/project/sphinxcontrib-websupport" + }, + { + "name": "spindry", + "uri": "https://pypi.org/project/spindry" + }, + { + "name": "spinners", + "uri": "https://pypi.org/project/spinners" + }, + { + "name": "splunk-handler", + "uri": "https://pypi.org/project/splunk-handler" + }, + { + "name": "splunk-sdk", + "uri": "https://pypi.org/project/splunk-sdk" + }, + { + "name": "spotinst-agent", + "uri": "https://pypi.org/project/spotinst-agent" + }, + { + "name": "sql-metadata", + "uri": "https://pypi.org/project/sql-metadata" + }, + { + "name": "sqlalchemy", + "uri": "https://pypi.org/project/sqlalchemy" + }, + { + "name": "sqlalchemy-bigquery", + "uri": "https://pypi.org/project/sqlalchemy-bigquery" + }, + { + "name": "sqlalchemy-jsonfield", + "uri": "https://pypi.org/project/sqlalchemy-jsonfield" + }, + { + "name": "sqlalchemy-migrate", + "uri": "https://pypi.org/project/sqlalchemy-migrate" + }, + { + "name": "sqlalchemy-redshift", + "uri": "https://pypi.org/project/sqlalchemy-redshift" + }, + { + "name": "sqlalchemy-spanner", + "uri": "https://pypi.org/project/sqlalchemy-spanner" + }, + { + "name": "sqlalchemy-utils", + "uri": "https://pypi.org/project/sqlalchemy-utils" + }, + { + "name": "sqlalchemy2-stubs", + "uri": "https://pypi.org/project/sqlalchemy2-stubs" + }, + { + "name": "sqlfluff", + "uri": "https://pypi.org/project/sqlfluff" + }, + { + "name": "sqlfluff-templater-dbt", + "uri": "https://pypi.org/project/sqlfluff-templater-dbt" + }, + { + "name": "sqlglot", + "uri": "https://pypi.org/project/sqlglot" + }, + { + "name": "sqlglotrs", + "uri": "https://pypi.org/project/sqlglotrs" + }, + { + "name": "sqlite-utils", + "uri": "https://pypi.org/project/sqlite-utils" + }, + { + "name": "sqlitedict", + "uri": "https://pypi.org/project/sqlitedict" + }, + { + "name": "sqllineage", + "uri": "https://pypi.org/project/sqllineage" + }, + { + "name": "sqlmodel", + "uri": "https://pypi.org/project/sqlmodel" + }, + { + "name": "sqlparams", + "uri": "https://pypi.org/project/sqlparams" + }, + { + "name": "sqlparse", + "uri": "https://pypi.org/project/sqlparse" + }, + { + "name": "srsly", + "uri": "https://pypi.org/project/srsly" + }, + { + "name": "sse-starlette", + "uri": "https://pypi.org/project/sse-starlette" + }, + { + "name": "sseclient-py", + "uri": "https://pypi.org/project/sseclient-py" + }, + { + "name": "sshpubkeys", + "uri": "https://pypi.org/project/sshpubkeys" + }, + { + "name": "sshtunnel", + "uri": "https://pypi.org/project/sshtunnel" + }, + { + "name": "stack-data", + "uri": "https://pypi.org/project/stack-data" + }, + { + "name": "stanio", + "uri": "https://pypi.org/project/stanio" + }, + { + "name": "starkbank-ecdsa", + "uri": "https://pypi.org/project/starkbank-ecdsa" + }, + { + "name": "starlette", + "uri": "https://pypi.org/project/starlette" + }, + { + "name": "starlette-exporter", + "uri": "https://pypi.org/project/starlette-exporter" + }, + { + "name": "statsd", + "uri": "https://pypi.org/project/statsd" + }, + { + "name": "statsforecast", + "uri": "https://pypi.org/project/statsforecast" + }, + { + "name": "statsmodels", + "uri": "https://pypi.org/project/statsmodels" + }, + { + "name": "std-uritemplate", + "uri": "https://pypi.org/project/std-uritemplate" + }, + { + "name": "stdlib-list", + "uri": "https://pypi.org/project/stdlib-list" + }, + { + "name": "stdlibs", + "uri": "https://pypi.org/project/stdlibs" + }, + { + "name": "stepfunctions", + "uri": "https://pypi.org/project/stepfunctions" + }, + { + "name": "stevedore", + "uri": "https://pypi.org/project/stevedore" + }, + { + "name": "stk", + "uri": "https://pypi.org/project/stk" + }, + { + "name": "stko", + "uri": "https://pypi.org/project/stko" + }, + { + "name": "stomp-py", + "uri": "https://pypi.org/project/stomp-py" + }, + { + "name": "stone", + "uri": "https://pypi.org/project/stone" + }, + { + "name": "strawberry-graphql", + "uri": "https://pypi.org/project/strawberry-graphql" + }, + { + "name": "streamerate", + "uri": "https://pypi.org/project/streamerate" + }, + { + "name": "streamlit", + "uri": "https://pypi.org/project/streamlit" + }, + { + "name": "strenum", + "uri": "https://pypi.org/project/strenum" + }, + { + "name": "strict-rfc3339", + "uri": "https://pypi.org/project/strict-rfc3339" + }, + { + "name": "strictyaml", + "uri": "https://pypi.org/project/strictyaml" + }, + { + "name": "stringcase", + "uri": "https://pypi.org/project/stringcase" + }, + { + "name": "strip-hints", + "uri": "https://pypi.org/project/strip-hints" + }, + { + "name": "stripe", + "uri": "https://pypi.org/project/stripe" + }, + { + "name": "striprtf", + "uri": "https://pypi.org/project/striprtf" + }, + { + "name": "structlog", + "uri": "https://pypi.org/project/structlog" + }, + { + "name": "subprocess-tee", + "uri": "https://pypi.org/project/subprocess-tee" + }, + { + "name": "subprocess32", + "uri": "https://pypi.org/project/subprocess32" + }, + { + "name": "sudachidict-core", + "uri": "https://pypi.org/project/sudachidict-core" + }, + { + "name": "sudachipy", + "uri": "https://pypi.org/project/sudachipy" + }, + { + "name": "suds-community", + "uri": "https://pypi.org/project/suds-community" + }, + { + "name": "suds-jurko", + "uri": "https://pypi.org/project/suds-jurko" + }, + { + "name": "suds-py3", + "uri": "https://pypi.org/project/suds-py3" + }, + { + "name": "supabase", + "uri": "https://pypi.org/project/supabase" + }, + { + "name": "supafunc", + "uri": "https://pypi.org/project/supafunc" + }, + { + "name": "supervision", + "uri": "https://pypi.org/project/supervision" + }, + { + "name": "supervisor", + "uri": "https://pypi.org/project/supervisor" + }, + { + "name": "svglib", + "uri": "https://pypi.org/project/svglib" + }, + { + "name": "svgwrite", + "uri": "https://pypi.org/project/svgwrite" + }, + { + "name": "swagger-spec-validator", + "uri": "https://pypi.org/project/swagger-spec-validator" + }, + { + "name": "swagger-ui-bundle", + "uri": "https://pypi.org/project/swagger-ui-bundle" + }, + { + "name": "swebench", + "uri": "https://pypi.org/project/swebench" + }, + { + "name": "swifter", + "uri": "https://pypi.org/project/swifter" + }, + { + "name": "symengine", + "uri": "https://pypi.org/project/symengine" + }, + { + "name": "sympy", + "uri": "https://pypi.org/project/sympy" + }, + { + "name": "table-meta", + "uri": "https://pypi.org/project/table-meta" + }, + { + "name": "tableau-api-lib", + "uri": "https://pypi.org/project/tableau-api-lib" + }, + { + "name": "tableauhyperapi", + "uri": "https://pypi.org/project/tableauhyperapi" + }, + { + "name": "tableauserverclient", + "uri": "https://pypi.org/project/tableauserverclient" + }, + { + "name": "tabledata", + "uri": "https://pypi.org/project/tabledata" + }, + { + "name": "tables", + "uri": "https://pypi.org/project/tables" + }, + { + "name": "tablib", + "uri": "https://pypi.org/project/tablib" + }, + { + "name": "tabulate", + "uri": "https://pypi.org/project/tabulate" + }, + { + "name": "tangled-up-in-unicode", + "uri": "https://pypi.org/project/tangled-up-in-unicode" + }, + { + "name": "tb-nightly", + "uri": "https://pypi.org/project/tb-nightly" + }, + { + "name": "tbats", + "uri": "https://pypi.org/project/tbats" + }, + { + "name": "tblib", + "uri": "https://pypi.org/project/tblib" + }, + { + "name": "tcolorpy", + "uri": "https://pypi.org/project/tcolorpy" + }, + { + "name": "tdqm", + "uri": "https://pypi.org/project/tdqm" + }, + { + "name": "tecton", + "uri": "https://pypi.org/project/tecton" + }, + { + "name": "tempita", + "uri": "https://pypi.org/project/tempita" + }, + { + "name": "tempora", + "uri": "https://pypi.org/project/tempora" + }, + { + "name": "temporalio", + "uri": "https://pypi.org/project/temporalio" + }, + { + "name": "tenacity", + "uri": "https://pypi.org/project/tenacity" + }, + { + "name": "tensorboard", + "uri": "https://pypi.org/project/tensorboard" + }, + { + "name": "tensorboard-data-server", + "uri": "https://pypi.org/project/tensorboard-data-server" + }, + { + "name": "tensorboard-plugin-wit", + "uri": "https://pypi.org/project/tensorboard-plugin-wit" + }, + { + "name": "tensorboardx", + "uri": "https://pypi.org/project/tensorboardx" + }, + { + "name": "tensorflow", + "uri": "https://pypi.org/project/tensorflow" + }, + { + "name": "tensorflow-addons", + "uri": "https://pypi.org/project/tensorflow-addons" + }, + { + "name": "tensorflow-cpu", + "uri": "https://pypi.org/project/tensorflow-cpu" + }, + { + "name": "tensorflow-datasets", + "uri": "https://pypi.org/project/tensorflow-datasets" + }, + { + "name": "tensorflow-estimator", + "uri": "https://pypi.org/project/tensorflow-estimator" + }, + { + "name": "tensorflow-hub", + "uri": "https://pypi.org/project/tensorflow-hub" + }, + { + "name": "tensorflow-intel", + "uri": "https://pypi.org/project/tensorflow-intel" + }, + { + "name": "tensorflow-io", + "uri": "https://pypi.org/project/tensorflow-io" + }, + { + "name": "tensorflow-io-gcs-filesystem", + "uri": "https://pypi.org/project/tensorflow-io-gcs-filesystem" + }, + { + "name": "tensorflow-metadata", + "uri": "https://pypi.org/project/tensorflow-metadata" + }, + { + "name": "tensorflow-model-optimization", + "uri": "https://pypi.org/project/tensorflow-model-optimization" + }, + { + "name": "tensorflow-probability", + "uri": "https://pypi.org/project/tensorflow-probability" + }, + { + "name": "tensorflow-serving-api", + "uri": "https://pypi.org/project/tensorflow-serving-api" + }, + { + "name": "tensorflow-text", + "uri": "https://pypi.org/project/tensorflow-text" + }, + { + "name": "tensorflowonspark", + "uri": "https://pypi.org/project/tensorflowonspark" + }, + { + "name": "tensorstore", + "uri": "https://pypi.org/project/tensorstore" + }, + { + "name": "teradatasql", + "uri": "https://pypi.org/project/teradatasql" + }, + { + "name": "teradatasqlalchemy", + "uri": "https://pypi.org/project/teradatasqlalchemy" + }, + { + "name": "termcolor", + "uri": "https://pypi.org/project/termcolor" + }, + { + "name": "terminado", + "uri": "https://pypi.org/project/terminado" + }, + { + "name": "terminaltables", + "uri": "https://pypi.org/project/terminaltables" + }, + { + "name": "testcontainers", + "uri": "https://pypi.org/project/testcontainers" + }, + { + "name": "testfixtures", + "uri": "https://pypi.org/project/testfixtures" + }, + { + "name": "testpath", + "uri": "https://pypi.org/project/testpath" + }, + { + "name": "testtools", + "uri": "https://pypi.org/project/testtools" + }, + { + "name": "text-unidecode", + "uri": "https://pypi.org/project/text-unidecode" + }, + { + "name": "textblob", + "uri": "https://pypi.org/project/textblob" + }, + { + "name": "textdistance", + "uri": "https://pypi.org/project/textdistance" + }, + { + "name": "textparser", + "uri": "https://pypi.org/project/textparser" + }, + { + "name": "texttable", + "uri": "https://pypi.org/project/texttable" + }, + { + "name": "textual", + "uri": "https://pypi.org/project/textual" + }, + { + "name": "textwrap3", + "uri": "https://pypi.org/project/textwrap3" + }, + { + "name": "tf-keras", + "uri": "https://pypi.org/project/tf-keras" + }, + { + "name": "tfx-bsl", + "uri": "https://pypi.org/project/tfx-bsl" + }, + { + "name": "thefuzz", + "uri": "https://pypi.org/project/thefuzz" + }, + { + "name": "thinc", + "uri": "https://pypi.org/project/thinc" + }, + { + "name": "thop", + "uri": "https://pypi.org/project/thop" + }, + { + "name": "threadpoolctl", + "uri": "https://pypi.org/project/threadpoolctl" + }, + { + "name": "thrift", + "uri": "https://pypi.org/project/thrift" + }, + { + "name": "thrift-sasl", + "uri": "https://pypi.org/project/thrift-sasl" + }, + { + "name": "throttlex", + "uri": "https://pypi.org/project/throttlex" + }, + { + "name": "tifffile", + "uri": "https://pypi.org/project/tifffile" + }, + { + "name": "tiktoken", + "uri": "https://pypi.org/project/tiktoken" + }, + { + "name": "time-machine", + "uri": "https://pypi.org/project/time-machine" + }, + { + "name": "timeout-decorator", + "uri": "https://pypi.org/project/timeout-decorator" + }, + { + "name": "timezonefinder", + "uri": "https://pypi.org/project/timezonefinder" + }, + { + "name": "timm", + "uri": "https://pypi.org/project/timm" + }, + { + "name": "tink", + "uri": "https://pypi.org/project/tink" + }, + { + "name": "tinycss2", + "uri": "https://pypi.org/project/tinycss2" + }, + { + "name": "tinydb", + "uri": "https://pypi.org/project/tinydb" + }, + { + "name": "tippo", + "uri": "https://pypi.org/project/tippo" + }, + { + "name": "tk", + "uri": "https://pypi.org/project/tk" + }, + { + "name": "tld", + "uri": "https://pypi.org/project/tld" + }, + { + "name": "tldextract", + "uri": "https://pypi.org/project/tldextract" + }, + { + "name": "tlparse", + "uri": "https://pypi.org/project/tlparse" + }, + { + "name": "tokenize-rt", + "uri": "https://pypi.org/project/tokenize-rt" + }, + { + "name": "tokenizers", + "uri": "https://pypi.org/project/tokenizers" + }, + { + "name": "tomesd", + "uri": "https://pypi.org/project/tomesd" + }, + { + "name": "toml", + "uri": "https://pypi.org/project/toml" + }, + { + "name": "tomli", + "uri": "https://pypi.org/project/tomli" + }, + { + "name": "tomli-w", + "uri": "https://pypi.org/project/tomli-w" + }, + { + "name": "tomlkit", + "uri": "https://pypi.org/project/tomlkit" + }, + { + "name": "toolz", + "uri": "https://pypi.org/project/toolz" + }, + { + "name": "toposort", + "uri": "https://pypi.org/project/toposort" + }, + { + "name": "torch", + "uri": "https://pypi.org/project/torch" + }, + { + "name": "torch-audiomentations", + "uri": "https://pypi.org/project/torch-audiomentations" + }, + { + "name": "torch-model-archiver", + "uri": "https://pypi.org/project/torch-model-archiver" + }, + { + "name": "torch-pitch-shift", + "uri": "https://pypi.org/project/torch-pitch-shift" + }, + { + "name": "torchaudio", + "uri": "https://pypi.org/project/torchaudio" + }, + { + "name": "torchdiffeq", + "uri": "https://pypi.org/project/torchdiffeq" + }, + { + "name": "torchmetrics", + "uri": "https://pypi.org/project/torchmetrics" + }, + { + "name": "torchsde", + "uri": "https://pypi.org/project/torchsde" + }, + { + "name": "torchtext", + "uri": "https://pypi.org/project/torchtext" + }, + { + "name": "torchvision", + "uri": "https://pypi.org/project/torchvision" + }, + { + "name": "tornado", + "uri": "https://pypi.org/project/tornado" + }, + { + "name": "tox", + "uri": "https://pypi.org/project/tox" + }, + { + "name": "tqdm", + "uri": "https://pypi.org/project/tqdm" + }, + { + "name": "traceback2", + "uri": "https://pypi.org/project/traceback2" + }, + { + "name": "trafilatura", + "uri": "https://pypi.org/project/trafilatura" + }, + { + "name": "trailrunner", + "uri": "https://pypi.org/project/trailrunner" + }, + { + "name": "traitlets", + "uri": "https://pypi.org/project/traitlets" + }, + { + "name": "traittypes", + "uri": "https://pypi.org/project/traittypes" + }, + { + "name": "trampoline", + "uri": "https://pypi.org/project/trampoline" + }, + { + "name": "transaction", + "uri": "https://pypi.org/project/transaction" + }, + { + "name": "transformers", + "uri": "https://pypi.org/project/transformers" + }, + { + "name": "transitions", + "uri": "https://pypi.org/project/transitions" + }, + { + "name": "translate", + "uri": "https://pypi.org/project/translate" + }, + { + "name": "translationstring", + "uri": "https://pypi.org/project/translationstring" + }, + { + "name": "tree-sitter", + "uri": "https://pypi.org/project/tree-sitter" + }, + { + "name": "tree-sitter-python", + "uri": "https://pypi.org/project/tree-sitter-python" + }, + { + "name": "treelib", + "uri": "https://pypi.org/project/treelib" + }, + { + "name": "triad", + "uri": "https://pypi.org/project/triad" + }, + { + "name": "trimesh", + "uri": "https://pypi.org/project/trimesh" + }, + { + "name": "trino", + "uri": "https://pypi.org/project/trino" + }, + { + "name": "trio", + "uri": "https://pypi.org/project/trio" + }, + { + "name": "trio-websocket", + "uri": "https://pypi.org/project/trio-websocket" + }, + { + "name": "triton", + "uri": "https://pypi.org/project/triton" + }, + { + "name": "tritonclient", + "uri": "https://pypi.org/project/tritonclient" + }, + { + "name": "trl", + "uri": "https://pypi.org/project/trl" + }, + { + "name": "troposphere", + "uri": "https://pypi.org/project/troposphere" + }, + { + "name": "trove-classifiers", + "uri": "https://pypi.org/project/trove-classifiers" + }, + { + "name": "truststore", + "uri": "https://pypi.org/project/truststore" + }, + { + "name": "tsx", + "uri": "https://pypi.org/project/tsx" + }, + { + "name": "tweepy", + "uri": "https://pypi.org/project/tweepy" + }, + { + "name": "twilio", + "uri": "https://pypi.org/project/twilio" + }, + { + "name": "twine", + "uri": "https://pypi.org/project/twine" + }, + { + "name": "twisted", + "uri": "https://pypi.org/project/twisted" + }, + { + "name": "txaio", + "uri": "https://pypi.org/project/txaio" + }, + { + "name": "typed-ast", + "uri": "https://pypi.org/project/typed-ast" + }, + { + "name": "typedload", + "uri": "https://pypi.org/project/typedload" + }, + { + "name": "typeguard", + "uri": "https://pypi.org/project/typeguard" + }, + { + "name": "typeid-python", + "uri": "https://pypi.org/project/typeid-python" + }, + { + "name": "typepy", + "uri": "https://pypi.org/project/typepy" + }, + { + "name": "typer", + "uri": "https://pypi.org/project/typer" + }, + { + "name": "types-aiobotocore", + "uri": "https://pypi.org/project/types-aiobotocore" + }, + { + "name": "types-aiobotocore-s3", + "uri": "https://pypi.org/project/types-aiobotocore-s3" + }, + { + "name": "types-awscrt", + "uri": "https://pypi.org/project/types-awscrt" + }, + { + "name": "types-beautifulsoup4", + "uri": "https://pypi.org/project/types-beautifulsoup4" + }, + { + "name": "types-cachetools", + "uri": "https://pypi.org/project/types-cachetools" + }, + { + "name": "types-cffi", + "uri": "https://pypi.org/project/types-cffi" + }, + { + "name": "types-colorama", + "uri": "https://pypi.org/project/types-colorama" + }, + { + "name": "types-cryptography", + "uri": "https://pypi.org/project/types-cryptography" + }, + { + "name": "types-dataclasses", + "uri": "https://pypi.org/project/types-dataclasses" + }, + { + "name": "types-decorator", + "uri": "https://pypi.org/project/types-decorator" + }, + { + "name": "types-deprecated", + "uri": "https://pypi.org/project/types-deprecated" + }, + { + "name": "types-docutils", + "uri": "https://pypi.org/project/types-docutils" + }, + { + "name": "types-html5lib", + "uri": "https://pypi.org/project/types-html5lib" + }, + { + "name": "types-jinja2", + "uri": "https://pypi.org/project/types-jinja2" + }, + { + "name": "types-jsonschema", + "uri": "https://pypi.org/project/types-jsonschema" + }, + { + "name": "types-markdown", + "uri": "https://pypi.org/project/types-markdown" + }, + { + "name": "types-markupsafe", + "uri": "https://pypi.org/project/types-markupsafe" + }, + { + "name": "types-mock", + "uri": "https://pypi.org/project/types-mock" + }, + { + "name": "types-paramiko", + "uri": "https://pypi.org/project/types-paramiko" + }, + { + "name": "types-pillow", + "uri": "https://pypi.org/project/types-pillow" + }, + { + "name": "types-protobuf", + "uri": "https://pypi.org/project/types-protobuf" + }, + { + "name": "types-psutil", + "uri": "https://pypi.org/project/types-psutil" + }, + { + "name": "types-psycopg2", + "uri": "https://pypi.org/project/types-psycopg2" + }, + { + "name": "types-pygments", + "uri": "https://pypi.org/project/types-pygments" + }, + { + "name": "types-pyopenssl", + "uri": "https://pypi.org/project/types-pyopenssl" + }, + { + "name": "types-pyserial", + "uri": "https://pypi.org/project/types-pyserial" + }, + { + "name": "types-python-dateutil", + "uri": "https://pypi.org/project/types-python-dateutil" + }, + { + "name": "types-pytz", + "uri": "https://pypi.org/project/types-pytz" + }, + { + "name": "types-pyyaml", + "uri": "https://pypi.org/project/types-pyyaml" + }, + { + "name": "types-redis", + "uri": "https://pypi.org/project/types-redis" + }, + { + "name": "types-requests", + "uri": "https://pypi.org/project/types-requests" + }, + { + "name": "types-retry", + "uri": "https://pypi.org/project/types-retry" + }, + { + "name": "types-s3transfer", + "uri": "https://pypi.org/project/types-s3transfer" + }, + { + "name": "types-setuptools", + "uri": "https://pypi.org/project/types-setuptools" + }, + { + "name": "types-simplejson", + "uri": "https://pypi.org/project/types-simplejson" + }, + { + "name": "types-six", + "uri": "https://pypi.org/project/types-six" + }, + { + "name": "types-tabulate", + "uri": "https://pypi.org/project/types-tabulate" + }, + { + "name": "types-toml", + "uri": "https://pypi.org/project/types-toml" + }, + { + "name": "types-ujson", + "uri": "https://pypi.org/project/types-ujson" + }, + { + "name": "types-urllib3", + "uri": "https://pypi.org/project/types-urllib3" + }, + { + "name": "typing", + "uri": "https://pypi.org/project/typing" + }, + { + "name": "typing-extensions", + "uri": "https://pypi.org/project/typing-extensions" + }, + { + "name": "typing-inspect", + "uri": "https://pypi.org/project/typing-inspect" + }, + { + "name": "typing-utils", + "uri": "https://pypi.org/project/typing-utils" + }, + { + "name": "typish", + "uri": "https://pypi.org/project/typish" + }, + { + "name": "tyro", + "uri": "https://pypi.org/project/tyro" + }, + { + "name": "tzdata", + "uri": "https://pypi.org/project/tzdata" + }, + { + "name": "tzfpy", + "uri": "https://pypi.org/project/tzfpy" + }, + { + "name": "tzlocal", + "uri": "https://pypi.org/project/tzlocal" + }, + { + "name": "ua-parser", + "uri": "https://pypi.org/project/ua-parser" + }, + { + "name": "uamqp", + "uri": "https://pypi.org/project/uamqp" + }, + { + "name": "uc-micro-py", + "uri": "https://pypi.org/project/uc-micro-py" + }, + { + "name": "ufmt", + "uri": "https://pypi.org/project/ufmt" + }, + { + "name": "uhashring", + "uri": "https://pypi.org/project/uhashring" + }, + { + "name": "ujson", + "uri": "https://pypi.org/project/ujson" + }, + { + "name": "ultralytics", + "uri": "https://pypi.org/project/ultralytics" + }, + { + "name": "ultralytics-thop", + "uri": "https://pypi.org/project/ultralytics-thop" + }, + { + "name": "umap-learn", + "uri": "https://pypi.org/project/umap-learn" + }, + { + "name": "uncertainties", + "uri": "https://pypi.org/project/uncertainties" + }, + { + "name": "undetected-chromedriver", + "uri": "https://pypi.org/project/undetected-chromedriver" + }, + { + "name": "unearth", + "uri": "https://pypi.org/project/unearth" + }, + { + "name": "unicodecsv", + "uri": "https://pypi.org/project/unicodecsv" + }, + { + "name": "unidecode", + "uri": "https://pypi.org/project/unidecode" + }, + { + "name": "unidiff", + "uri": "https://pypi.org/project/unidiff" + }, + { + "name": "unittest-xml-reporting", + "uri": "https://pypi.org/project/unittest-xml-reporting" + }, + { + "name": "unittest2", + "uri": "https://pypi.org/project/unittest2" + }, + { + "name": "universal-pathlib", + "uri": "https://pypi.org/project/universal-pathlib" + }, + { + "name": "unstructured", + "uri": "https://pypi.org/project/unstructured" + }, + { + "name": "unstructured-client", + "uri": "https://pypi.org/project/unstructured-client" + }, + { + "name": "update-checker", + "uri": "https://pypi.org/project/update-checker" + }, + { + "name": "uplink", + "uri": "https://pypi.org/project/uplink" + }, + { + "name": "uproot", + "uri": "https://pypi.org/project/uproot" + }, + { + "name": "uptime-kuma-api", + "uri": "https://pypi.org/project/uptime-kuma-api" + }, + { + "name": "uri-template", + "uri": "https://pypi.org/project/uri-template" + }, + { + "name": "uritemplate", + "uri": "https://pypi.org/project/uritemplate" + }, + { + "name": "uritools", + "uri": "https://pypi.org/project/uritools" + }, + { + "name": "url-normalize", + "uri": "https://pypi.org/project/url-normalize" + }, + { + "name": "urllib3", + "uri": "https://pypi.org/project/urllib3" + }, + { + "name": "urllib3-secure-extra", + "uri": "https://pypi.org/project/urllib3-secure-extra" + }, + { + "name": "urwid", + "uri": "https://pypi.org/project/urwid" + }, + { + "name": "usaddress", + "uri": "https://pypi.org/project/usaddress" + }, + { + "name": "user-agents", + "uri": "https://pypi.org/project/user-agents" + }, + { + "name": "userpath", + "uri": "https://pypi.org/project/userpath" + }, + { + "name": "usort", + "uri": "https://pypi.org/project/usort" + }, + { + "name": "utilsforecast", + "uri": "https://pypi.org/project/utilsforecast" + }, + { + "name": "uuid", + "uri": "https://pypi.org/project/uuid" + }, + { + "name": "uuid6", + "uri": "https://pypi.org/project/uuid6" + }, + { + "name": "uv", + "uri": "https://pypi.org/project/uv" + }, + { + "name": "uvicorn", + "uri": "https://pypi.org/project/uvicorn" + }, + { + "name": "uvloop", + "uri": "https://pypi.org/project/uvloop" + }, + { + "name": "uwsgi", + "uri": "https://pypi.org/project/uwsgi" + }, + { + "name": "validate-email", + "uri": "https://pypi.org/project/validate-email" + }, + { + "name": "validators", + "uri": "https://pypi.org/project/validators" + }, + { + "name": "vcrpy", + "uri": "https://pypi.org/project/vcrpy" + }, + { + "name": "venusian", + "uri": "https://pypi.org/project/venusian" + }, + { + "name": "verboselogs", + "uri": "https://pypi.org/project/verboselogs" + }, + { + "name": "versioneer", + "uri": "https://pypi.org/project/versioneer" + }, + { + "name": "versioneer-518", + "uri": "https://pypi.org/project/versioneer-518" + }, + { + "name": "vertexai", + "uri": "https://pypi.org/project/vertexai" + }, + { + "name": "vine", + "uri": "https://pypi.org/project/vine" + }, + { + "name": "virtualenv", + "uri": "https://pypi.org/project/virtualenv" + }, + { + "name": "virtualenv-clone", + "uri": "https://pypi.org/project/virtualenv-clone" + }, + { + "name": "visions", + "uri": "https://pypi.org/project/visions" + }, + { + "name": "vllm", + "uri": "https://pypi.org/project/vllm" + }, + { + "name": "voluptuous", + "uri": "https://pypi.org/project/voluptuous" + }, + { + "name": "vtk", + "uri": "https://pypi.org/project/vtk" + }, + { + "name": "vulture", + "uri": "https://pypi.org/project/vulture" + }, + { + "name": "w3lib", + "uri": "https://pypi.org/project/w3lib" + }, + { + "name": "waitress", + "uri": "https://pypi.org/project/waitress" + }, + { + "name": "wand", + "uri": "https://pypi.org/project/wand" + }, + { + "name": "wandb", + "uri": "https://pypi.org/project/wandb" + }, + { + "name": "wasabi", + "uri": "https://pypi.org/project/wasabi" + }, + { + "name": "wasmtime", + "uri": "https://pypi.org/project/wasmtime" + }, + { + "name": "watchdog", + "uri": "https://pypi.org/project/watchdog" + }, + { + "name": "watchfiles", + "uri": "https://pypi.org/project/watchfiles" + }, + { + "name": "watchgod", + "uri": "https://pypi.org/project/watchgod" + }, + { + "name": "watchtower", + "uri": "https://pypi.org/project/watchtower" + }, + { + "name": "wcmatch", + "uri": "https://pypi.org/project/wcmatch" + }, + { + "name": "wcwidth", + "uri": "https://pypi.org/project/wcwidth" + }, + { + "name": "weasel", + "uri": "https://pypi.org/project/weasel" + }, + { + "name": "weasyprint", + "uri": "https://pypi.org/project/weasyprint" + }, + { + "name": "weaviate-client", + "uri": "https://pypi.org/project/weaviate-client" + }, + { + "name": "web3", + "uri": "https://pypi.org/project/web3" + }, + { + "name": "webargs", + "uri": "https://pypi.org/project/webargs" + }, + { + "name": "webcolors", + "uri": "https://pypi.org/project/webcolors" + }, + { + "name": "webdataset", + "uri": "https://pypi.org/project/webdataset" + }, + { + "name": "webdriver-manager", + "uri": "https://pypi.org/project/webdriver-manager" + }, + { + "name": "webencodings", + "uri": "https://pypi.org/project/webencodings" + }, + { + "name": "webhelpers2", + "uri": "https://pypi.org/project/webhelpers2" + }, + { + "name": "webob", + "uri": "https://pypi.org/project/webob" + }, + { + "name": "webrtcvad-wheels", + "uri": "https://pypi.org/project/webrtcvad-wheels" + }, + { + "name": "websocket-client", + "uri": "https://pypi.org/project/websocket-client" + }, + { + "name": "websockets", + "uri": "https://pypi.org/project/websockets" + }, + { + "name": "webtest", + "uri": "https://pypi.org/project/webtest" + }, + { + "name": "werkzeug", + "uri": "https://pypi.org/project/werkzeug" + }, + { + "name": "west", + "uri": "https://pypi.org/project/west" + }, + { + "name": "wget", + "uri": "https://pypi.org/project/wget" + }, + { + "name": "wheel", + "uri": "https://pypi.org/project/wheel" + }, + { + "name": "whitenoise", + "uri": "https://pypi.org/project/whitenoise" + }, + { + "name": "widgetsnbextension", + "uri": "https://pypi.org/project/widgetsnbextension" + }, + { + "name": "wikitextparser", + "uri": "https://pypi.org/project/wikitextparser" + }, + { + "name": "wirerope", + "uri": "https://pypi.org/project/wirerope" + }, + { + "name": "wmi", + "uri": "https://pypi.org/project/wmi" + }, + { + "name": "wmill", + "uri": "https://pypi.org/project/wmill" + }, + { + "name": "wordcloud", + "uri": "https://pypi.org/project/wordcloud" + }, + { + "name": "workalendar", + "uri": "https://pypi.org/project/workalendar" + }, + { + "name": "wrapt", + "uri": "https://pypi.org/project/wrapt" + }, + { + "name": "ws4py", + "uri": "https://pypi.org/project/ws4py" + }, + { + "name": "wsgiproxy2", + "uri": "https://pypi.org/project/wsgiproxy2" + }, + { + "name": "wsproto", + "uri": "https://pypi.org/project/wsproto" + }, + { + "name": "wtforms", + "uri": "https://pypi.org/project/wtforms" + }, + { + "name": "wurlitzer", + "uri": "https://pypi.org/project/wurlitzer" + }, + { + "name": "xarray", + "uri": "https://pypi.org/project/xarray" + }, + { + "name": "xarray-einstats", + "uri": "https://pypi.org/project/xarray-einstats" + }, + { + "name": "xatlas", + "uri": "https://pypi.org/project/xatlas" + }, + { + "name": "xattr", + "uri": "https://pypi.org/project/xattr" + }, + { + "name": "xformers", + "uri": "https://pypi.org/project/xformers" + }, + { + "name": "xgboost", + "uri": "https://pypi.org/project/xgboost" + }, + { + "name": "xhtml2pdf", + "uri": "https://pypi.org/project/xhtml2pdf" + }, + { + "name": "xlrd", + "uri": "https://pypi.org/project/xlrd" + }, + { + "name": "xlsxwriter", + "uri": "https://pypi.org/project/xlsxwriter" + }, + { + "name": "xlutils", + "uri": "https://pypi.org/project/xlutils" + }, + { + "name": "xlwt", + "uri": "https://pypi.org/project/xlwt" + }, + { + "name": "xmlschema", + "uri": "https://pypi.org/project/xmlschema" + }, + { + "name": "xmlsec", + "uri": "https://pypi.org/project/xmlsec" + }, + { + "name": "xmltodict", + "uri": "https://pypi.org/project/xmltodict" + }, + { + "name": "xmod", + "uri": "https://pypi.org/project/xmod" + }, + { + "name": "xmodem", + "uri": "https://pypi.org/project/xmodem" + }, + { + "name": "xxhash", + "uri": "https://pypi.org/project/xxhash" + }, + { + "name": "xyzservices", + "uri": "https://pypi.org/project/xyzservices" + }, + { + "name": "y-py", + "uri": "https://pypi.org/project/y-py" + }, + { + "name": "yacs", + "uri": "https://pypi.org/project/yacs" + }, + { + "name": "yamale", + "uri": "https://pypi.org/project/yamale" + }, + { + "name": "yamllint", + "uri": "https://pypi.org/project/yamllint" + }, + { + "name": "yapf", + "uri": "https://pypi.org/project/yapf" + }, + { + "name": "yappi", + "uri": "https://pypi.org/project/yappi" + }, + { + "name": "yarg", + "uri": "https://pypi.org/project/yarg" + }, + { + "name": "yarl", + "uri": "https://pypi.org/project/yarl" + }, + { + "name": "yarn-api-client", + "uri": "https://pypi.org/project/yarn-api-client" + }, + { + "name": "yaspin", + "uri": "https://pypi.org/project/yaspin" + }, + { + "name": "ydata-profiling", + "uri": "https://pypi.org/project/ydata-profiling" + }, + { + "name": "yfinance", + "uri": "https://pypi.org/project/yfinance" + }, + { + "name": "youtube-dl", + "uri": "https://pypi.org/project/youtube-dl" + }, + { + "name": "youtube-transcript-api", + "uri": "https://pypi.org/project/youtube-transcript-api" + }, + { + "name": "ypy-websocket", + "uri": "https://pypi.org/project/ypy-websocket" + }, + { + "name": "yq", + "uri": "https://pypi.org/project/yq" + }, + { + "name": "yt-dlp", + "uri": "https://pypi.org/project/yt-dlp" + }, + { + "name": "z3-solver", + "uri": "https://pypi.org/project/z3-solver" + }, + { + "name": "z3c-pt", + "uri": "https://pypi.org/project/z3c-pt" + }, + { + "name": "zarr", + "uri": "https://pypi.org/project/zarr" + }, + { + "name": "zc-lockfile", + "uri": "https://pypi.org/project/zc-lockfile" + }, + { + "name": "zconfig", + "uri": "https://pypi.org/project/zconfig" + }, + { + "name": "zeep", + "uri": "https://pypi.org/project/zeep" + }, + { + "name": "zenpy", + "uri": "https://pypi.org/project/zenpy" + }, + { + "name": "zeroconf", + "uri": "https://pypi.org/project/zeroconf" + }, + { + "name": "zexceptions", + "uri": "https://pypi.org/project/zexceptions" + }, + { + "name": "zha-quirks", + "uri": "https://pypi.org/project/zha-quirks" + }, + { + "name": "zict", + "uri": "https://pypi.org/project/zict" + }, + { + "name": "zigpy", + "uri": "https://pypi.org/project/zigpy" + }, + { + "name": "zigpy-deconz", + "uri": "https://pypi.org/project/zigpy-deconz" + }, + { + "name": "zigpy-xbee", + "uri": "https://pypi.org/project/zigpy-xbee" + }, + { + "name": "zigpy-znp", + "uri": "https://pypi.org/project/zigpy-znp" + }, + { + "name": "zipfile-deflate64", + "uri": "https://pypi.org/project/zipfile-deflate64" + }, + { + "name": "zipfile36", + "uri": "https://pypi.org/project/zipfile36" + }, + { + "name": "zipp", + "uri": "https://pypi.org/project/zipp" + }, + { + "name": "zodb", + "uri": "https://pypi.org/project/zodb" + }, + { + "name": "zodbpickle", + "uri": "https://pypi.org/project/zodbpickle" + }, + { + "name": "zope", + "uri": "https://pypi.org/project/zope" + }, + { + "name": "zope-annotation", + "uri": "https://pypi.org/project/zope-annotation" + }, + { + "name": "zope-browser", + "uri": "https://pypi.org/project/zope-browser" + }, + { + "name": "zope-browsermenu", + "uri": "https://pypi.org/project/zope-browsermenu" + }, + { + "name": "zope-browserpage", + "uri": "https://pypi.org/project/zope-browserpage" + }, + { + "name": "zope-browserresource", + "uri": "https://pypi.org/project/zope-browserresource" + }, + { + "name": "zope-cachedescriptors", + "uri": "https://pypi.org/project/zope-cachedescriptors" + }, + { + "name": "zope-component", + "uri": "https://pypi.org/project/zope-component" + }, + { + "name": "zope-configuration", + "uri": "https://pypi.org/project/zope-configuration" + }, + { + "name": "zope-container", + "uri": "https://pypi.org/project/zope-container" + }, + { + "name": "zope-contentprovider", + "uri": "https://pypi.org/project/zope-contentprovider" + }, + { + "name": "zope-contenttype", + "uri": "https://pypi.org/project/zope-contenttype" + }, + { + "name": "zope-datetime", + "uri": "https://pypi.org/project/zope-datetime" + }, + { + "name": "zope-deferredimport", + "uri": "https://pypi.org/project/zope-deferredimport" + }, + { + "name": "zope-deprecation", + "uri": "https://pypi.org/project/zope-deprecation" + }, + { + "name": "zope-dottedname", + "uri": "https://pypi.org/project/zope-dottedname" + }, + { + "name": "zope-event", + "uri": "https://pypi.org/project/zope-event" + }, + { + "name": "zope-exceptions", + "uri": "https://pypi.org/project/zope-exceptions" + }, + { + "name": "zope-filerepresentation", + "uri": "https://pypi.org/project/zope-filerepresentation" + }, + { + "name": "zope-globalrequest", + "uri": "https://pypi.org/project/zope-globalrequest" + }, + { + "name": "zope-hookable", + "uri": "https://pypi.org/project/zope-hookable" + }, + { + "name": "zope-i18n", + "uri": "https://pypi.org/project/zope-i18n" + }, + { + "name": "zope-i18nmessageid", + "uri": "https://pypi.org/project/zope-i18nmessageid" + }, + { + "name": "zope-interface", + "uri": "https://pypi.org/project/zope-interface" + }, + { + "name": "zope-lifecycleevent", + "uri": "https://pypi.org/project/zope-lifecycleevent" + }, + { + "name": "zope-location", + "uri": "https://pypi.org/project/zope-location" + }, + { + "name": "zope-pagetemplate", + "uri": "https://pypi.org/project/zope-pagetemplate" + }, + { + "name": "zope-processlifetime", + "uri": "https://pypi.org/project/zope-processlifetime" + }, + { + "name": "zope-proxy", + "uri": "https://pypi.org/project/zope-proxy" + }, + { + "name": "zope-ptresource", + "uri": "https://pypi.org/project/zope-ptresource" + }, + { + "name": "zope-publisher", + "uri": "https://pypi.org/project/zope-publisher" + }, + { + "name": "zope-schema", + "uri": "https://pypi.org/project/zope-schema" + }, + { + "name": "zope-security", + "uri": "https://pypi.org/project/zope-security" + }, + { + "name": "zope-sequencesort", + "uri": "https://pypi.org/project/zope-sequencesort" + }, + { + "name": "zope-site", + "uri": "https://pypi.org/project/zope-site" + }, + { + "name": "zope-size", + "uri": "https://pypi.org/project/zope-size" + }, + { + "name": "zope-structuredtext", + "uri": "https://pypi.org/project/zope-structuredtext" + }, + { + "name": "zope-tal", + "uri": "https://pypi.org/project/zope-tal" + }, + { + "name": "zope-tales", + "uri": "https://pypi.org/project/zope-tales" + }, + { + "name": "zope-testbrowser", + "uri": "https://pypi.org/project/zope-testbrowser" + }, + { + "name": "zope-testing", + "uri": "https://pypi.org/project/zope-testing" + }, + { + "name": "zope-traversing", + "uri": "https://pypi.org/project/zope-traversing" + }, + { + "name": "zope-viewlet", + "uri": "https://pypi.org/project/zope-viewlet" + }, + { + "name": "zopfli", + "uri": "https://pypi.org/project/zopfli" + }, + { + "name": "zstandard", + "uri": "https://pypi.org/project/zstandard" + }, + { + "name": "zstd", + "uri": "https://pypi.org/project/zstd" + }, + { + "name": "zthreading", + "uri": "https://pypi.org/project/zthreading" + } +] \ No newline at end of file diff --git a/files/conda_packages.json b/files/conda_packages.json new file mode 100644 index 0000000..fa944bd --- /dev/null +++ b/files/conda_packages.json @@ -0,0 +1,3584 @@ +[ + { "name": "7zip", "uri": "https://anaconda.org/anaconda/7zip", "description": "7-Zip is a file archiver with a high compression ratio." }, + { "name": "abseil-cpp", "uri": "https://anaconda.org/anaconda/abseil-cpp", "description": "Abseil Common Libraries (C++)" }, + { "name": "absl-py", "uri": "https://anaconda.org/anaconda/absl-py", "description": "Abseil Common Libraries (Python)" }, + { "name": "access", "uri": "https://anaconda.org/anaconda/access", "description": "classical and novel measures of spatial accessibility to services" }, + { "name": "acl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/acl-amzn2-aarch64", "description": "(CDT) Access control list utilities" }, + { "name": "adagio", "uri": "https://anaconda.org/anaconda/adagio", "description": "A Dag IO framework for Fugue projects." }, + { "name": "adal", "uri": "https://anaconda.org/anaconda/adal", "description": "The ADAL for Python library makes it easy for python application to authenticate\nto Azure Active Directory (AAD) in order to access AAD protected web resources." }, + { "name": "adtk", "uri": "https://anaconda.org/anaconda/adtk", "description": "A package for unsupervised time series anomaly detection" }, + { "name": "adwaita-icon-theme", "uri": "https://anaconda.org/anaconda/adwaita-icon-theme", "description": "The default icon theme used by the GNOME desktop" }, + { "name": "aenum", "uri": "https://anaconda.org/anaconda/aenum", "description": "Advanced Enumerations (compatible with Python's stdlib Enum), NamedTuples, and NamedConstants" }, + { "name": "aext-assistant", "uri": "https://anaconda.org/anaconda/aext-assistant", "description": "Anaconda extensions assistant library" }, + { "name": "aext-assistant-server", "uri": "https://anaconda.org/anaconda/aext-assistant-server", "description": "Anaconda extensions assistant server" }, + { "name": "aext-core", "uri": "https://anaconda.org/anaconda/aext-core", "description": "Anaconda extensions core library" }, + { "name": "aext-core-server", "uri": "https://anaconda.org/anaconda/aext-core-server", "description": "Anaconda Toolbox backend lib core server component" }, + { "name": "aext-panels", "uri": "https://anaconda.org/anaconda/aext-panels", "description": "The aext-panels component of anaconda-toolbox" }, + { "name": "aext-panels-server", "uri": "https://anaconda.org/anaconda/aext-panels-server", "description": "The aext-panels-server component of anaconda-toolbox" }, + { "name": "aext-project-filebrowser-server", "uri": "https://anaconda.org/anaconda/aext-project-filebrowser-server", "description": "The aext-project-filebrowser-server component of anaconda-toolbox" }, + { "name": "aext-share-notebook", "uri": "https://anaconda.org/anaconda/aext-share-notebook", "description": "The aext-share-notebook component of anaconda-toolbox" }, + { "name": "aext-share-notebook-server", "uri": "https://anaconda.org/anaconda/aext-share-notebook-server", "description": "Anaconda extensions share notebook server" }, + { "name": "aext-shared", "uri": "https://anaconda.org/anaconda/aext-shared", "description": "Anaconda extensions shared library" }, + { "name": "agate", "uri": "https://anaconda.org/anaconda/agate", "description": "A data analysis library that is optimized for humans instead of machines." }, + { "name": "agate-dbf", "uri": "https://anaconda.org/anaconda/agate-dbf", "description": "agate-dbf adds read support for dbf files to agate." }, + { "name": "agate-excel", "uri": "https://anaconda.org/anaconda/agate-excel", "description": "agate-excel adds read support for Excel files (xls and xlsx) to agate." }, + { "name": "aiobotocore", "uri": "https://anaconda.org/anaconda/aiobotocore", "description": "Async client for aws services using botocore and aiohttp" }, + { "name": "aiodns", "uri": "https://anaconda.org/anaconda/aiodns", "description": "Simple DNS resolver for asyncio" }, + { "name": "aiofiles", "uri": "https://anaconda.org/anaconda/aiofiles", "description": "File support for asyncio" }, + { "name": "aiohappyeyeballs", "uri": "https://anaconda.org/anaconda/aiohappyeyeballs", "description": "Happy Eyeballs for asyncio" }, + { "name": "aiohttp", "uri": "https://anaconda.org/anaconda/aiohttp", "description": "Async http client/server framework (asyncio)" }, + { "name": "aiohttp-cors", "uri": "https://anaconda.org/anaconda/aiohttp-cors", "description": "CORS support for aiohttp" }, + { "name": "aiohttp-jinja2", "uri": "https://anaconda.org/anaconda/aiohttp-jinja2", "description": "jinja2 template renderer for aiohttp.web (http server for asyncio)" }, + { "name": "aioitertools", "uri": "https://anaconda.org/anaconda/aioitertools", "description": "asyncio version of the standard multiprocessing module" }, + { "name": "aiopg", "uri": "https://anaconda.org/anaconda/aiopg", "description": "Postgres integration with asyncio." }, + { "name": "aioredis", "uri": "https://anaconda.org/anaconda/aioredis", "description": "asyncio (PEP 3156) Redis support" }, + { "name": "aiorwlock", "uri": "https://anaconda.org/anaconda/aiorwlock", "description": "Read write lock for asyncio." }, + { "name": "aiosignal", "uri": "https://anaconda.org/anaconda/aiosignal", "description": "aiosignal: a list of registered asynchronous callbacks" }, + { "name": "aiosqlite", "uri": "https://anaconda.org/anaconda/aiosqlite", "description": "asyncio bridge to the standard sqlite3 module" }, + { "name": "airflow", "uri": "https://anaconda.org/anaconda/airflow", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-apache-atlas", "uri": "https://anaconda.org/anaconda/airflow-with-apache-atlas", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-apache-webhdfs", "uri": "https://anaconda.org/anaconda/airflow-with-apache-webhdfs", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-async", "uri": "https://anaconda.org/anaconda/airflow-with-async", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-azure-mgmt-containerinstance", "uri": "https://anaconda.org/anaconda/airflow-with-azure-mgmt-containerinstance", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-azure_blob_storage", "uri": "https://anaconda.org/anaconda/airflow-with-azure_blob_storage", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-azure_cosmos", "uri": "https://anaconda.org/anaconda/airflow-with-azure_cosmos", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-cassandra", "uri": "https://anaconda.org/anaconda/airflow-with-cassandra", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-celery", "uri": "https://anaconda.org/anaconda/airflow-with-celery", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-cgroups", "uri": "https://anaconda.org/anaconda/airflow-with-cgroups", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-cloudant", "uri": "https://anaconda.org/anaconda/airflow-with-cloudant", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-cncf-kubernetes", "uri": "https://anaconda.org/anaconda/airflow-with-cncf-kubernetes", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-crypto", "uri": "https://anaconda.org/anaconda/airflow-with-crypto", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-dask", "uri": "https://anaconda.org/anaconda/airflow-with-dask", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-databricks", "uri": "https://anaconda.org/anaconda/airflow-with-databricks", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-datadog", "uri": "https://anaconda.org/anaconda/airflow-with-datadog", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-deprecated-api", "uri": "https://anaconda.org/anaconda/airflow-with-deprecated-api", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-docker", "uri": "https://anaconda.org/anaconda/airflow-with-docker", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-druid", "uri": "https://anaconda.org/anaconda/airflow-with-druid", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-elasticsearch", "uri": "https://anaconda.org/anaconda/airflow-with-elasticsearch", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-emr", "uri": "https://anaconda.org/anaconda/airflow-with-emr", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-github_enterprise", "uri": "https://anaconda.org/anaconda/airflow-with-github_enterprise", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-google_auth", "uri": "https://anaconda.org/anaconda/airflow-with-google_auth", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-hdfs", "uri": "https://anaconda.org/anaconda/airflow-with-hdfs", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-jdbc", "uri": "https://anaconda.org/anaconda/airflow-with-jdbc", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-jenkins", "uri": "https://anaconda.org/anaconda/airflow-with-jenkins", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-jira", "uri": "https://anaconda.org/anaconda/airflow-with-jira", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-kerberos", "uri": "https://anaconda.org/anaconda/airflow-with-kerberos", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-kubernetes", "uri": "https://anaconda.org/anaconda/airflow-with-kubernetes", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-ldap", "uri": "https://anaconda.org/anaconda/airflow-with-ldap", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-leveldb", "uri": "https://anaconda.org/anaconda/airflow-with-leveldb", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-mongo", "uri": "https://anaconda.org/anaconda/airflow-with-mongo", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-mssql", "uri": "https://anaconda.org/anaconda/airflow-with-mssql", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-mysql", "uri": "https://anaconda.org/anaconda/airflow-with-mysql", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-pandas", "uri": "https://anaconda.org/anaconda/airflow-with-pandas", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-password", "uri": "https://anaconda.org/anaconda/airflow-with-password", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-postgres", "uri": "https://anaconda.org/anaconda/airflow-with-postgres", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-qds", "uri": "https://anaconda.org/anaconda/airflow-with-qds", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-rabbitmq", "uri": "https://anaconda.org/anaconda/airflow-with-rabbitmq", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-redis", "uri": "https://anaconda.org/anaconda/airflow-with-redis", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-salesforce", "uri": "https://anaconda.org/anaconda/airflow-with-salesforce", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-samba", "uri": "https://anaconda.org/anaconda/airflow-with-samba", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-sendgrid", "uri": "https://anaconda.org/anaconda/airflow-with-sendgrid", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-sentry", "uri": "https://anaconda.org/anaconda/airflow-with-sentry", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-slack", "uri": "https://anaconda.org/anaconda/airflow-with-slack", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-ssh", "uri": "https://anaconda.org/anaconda/airflow-with-ssh", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-statsd", "uri": "https://anaconda.org/anaconda/airflow-with-statsd", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-vertica", "uri": "https://anaconda.org/anaconda/airflow-with-vertica", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-virtualenv", "uri": "https://anaconda.org/anaconda/airflow-with-virtualenv", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-webhdfs", "uri": "https://anaconda.org/anaconda/airflow-with-webhdfs", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "airflow-with-winrm", "uri": "https://anaconda.org/anaconda/airflow-with-winrm", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "alabaster", "uri": "https://anaconda.org/anaconda/alabaster", "description": "Lightweight, configurable Sphinx theme" }, + { "name": "alembic", "uri": "https://anaconda.org/anaconda/alembic", "description": "A database migration tool for SQLAlchemy." }, + { "name": "allure-behave", "uri": "https://anaconda.org/anaconda/allure-behave", "description": "Allure integrations for Python test frameworks" }, + { "name": "allure-nose2", "uri": "https://anaconda.org/anaconda/allure-nose2", "description": "Allure integrations for Python test frameworks" }, + { "name": "allure-pytest", "uri": "https://anaconda.org/anaconda/allure-pytest", "description": "Allure integrations for Python test frameworks" }, + { "name": "allure-pytest-bdd", "uri": "https://anaconda.org/anaconda/allure-pytest-bdd", "description": "Allure integrations for Python test frameworks" }, + { "name": "allure-python-commons", "uri": "https://anaconda.org/anaconda/allure-python-commons", "description": "Allure integrations for Python test frameworks" }, + { "name": "allure-robotframework", "uri": "https://anaconda.org/anaconda/allure-robotframework", "description": "Allure integrations for Python test frameworks" }, + { "name": "alsa-lib-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/alsa-lib-amzn2-aarch64", "description": "(CDT) The Advanced Linux Sound Architecture (ALSA) library" }, + { "name": "alsa-lib-cos6-x86_64", "uri": "https://anaconda.org/anaconda/alsa-lib-cos6-x86_64", "description": "(CDT) The Advanced Linux Sound Architecture (ALSA) library" }, + { "name": "alsa-lib-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/alsa-lib-cos7-ppc64le", "description": "(CDT) The Advanced Linux Sound Architecture (ALSA) library" }, + { "name": "alsa-lib-cos7-s390x", "uri": "https://anaconda.org/anaconda/alsa-lib-cos7-s390x", "description": "(CDT) The Advanced Linux Sound Architecture (ALSA) library" }, + { "name": "alsa-lib-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/alsa-lib-devel-amzn2-aarch64", "description": "(CDT) Development files from the ALSA library" }, + { "name": "alsa-lib-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/alsa-lib-devel-cos7-ppc64le", "description": "(CDT) Development files from the ALSA library" }, + { "name": "alsa-lib-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/alsa-lib-devel-cos7-s390x", "description": "(CDT) Development files from the ALSA library" }, + { "name": "alsa-utils-cos6-i686", "uri": "https://anaconda.org/anaconda/alsa-utils-cos6-i686", "description": "(CDT) Advanced Linux Sound Architecture (ALSA) utilities" }, + { "name": "alsa-utils-cos6-x86_64", "uri": "https://anaconda.org/anaconda/alsa-utils-cos6-x86_64", "description": "(CDT) Advanced Linux Sound Architecture (ALSA) utilities" }, + { "name": "alsa-utils-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/alsa-utils-cos7-ppc64le", "description": "(CDT) Advanced Linux Sound Architecture (ALSA) utilities" }, + { "name": "altair", "uri": "https://anaconda.org/anaconda/altair", "description": "A declarative statistical visualization library for Python" }, + { "name": "altgraph", "uri": "https://anaconda.org/anaconda/altgraph", "description": "Python graph (network) package" }, + { "name": "ampl-mp", "uri": "https://anaconda.org/anaconda/ampl-mp", "description": "An open-source library for mathematical programming." }, + { "name": "amply", "uri": "https://anaconda.org/anaconda/amply", "description": "Amply allows you to load and manipulate AMPL/GLPK data as Python data structures" }, + { "name": "amqp", "uri": "https://anaconda.org/anaconda/amqp", "description": "Low-level AMQP client for Python (fork of amqplib)" }, + { "name": "anaconda", "uri": "https://anaconda.org/anaconda/anaconda", "description": "Simplifies package management and deployment of Anaconda" }, + { "name": "anaconda-anon-usage", "uri": "https://anaconda.org/anaconda/anaconda-anon-usage", "description": "basic anonymous telemetry for conda clients" }, + { "name": "anaconda-catalogs", "uri": "https://anaconda.org/anaconda/anaconda-catalogs", "description": "Client library to interface with Anaconda Cloud catalogs service" }, + { "name": "anaconda-clean", "uri": "https://anaconda.org/anaconda/anaconda-clean", "description": "Delete Anaconda configuration files" }, + { "name": "anaconda-cli-base", "uri": "https://anaconda.org/anaconda/anaconda-cli-base", "description": "A base CLI entrypoint supporting Anaconda CLI plugins" }, + { "name": "anaconda-client", "uri": "https://anaconda.org/anaconda/anaconda-client", "description": "anaconda.org command line client library" }, + { "name": "anaconda-cloud", "uri": "https://anaconda.org/anaconda/anaconda-cloud", "description": "Anaconda Cloud client tools" }, + { "name": "anaconda-cloud-auth", "uri": "https://anaconda.org/anaconda/anaconda-cloud-auth", "description": "A client auth library for Anaconda.cloud APIs" }, + { "name": "anaconda-cloud-cli", "uri": "https://anaconda.org/anaconda/anaconda-cloud-cli", "description": "The Anaconda Cloud CLI" }, + { "name": "anaconda-distribution-installer", "uri": "https://anaconda.org/anaconda/anaconda-distribution-installer", "description": "create installer from conda packages" }, + { "name": "anaconda-doc", "uri": "https://anaconda.org/anaconda/anaconda-doc", "description": "No Summary" }, + { "name": "anaconda-docs", "uri": "https://anaconda.org/anaconda/anaconda-docs", "description": "No Summary" }, + { "name": "anaconda-enterprise-cli", "uri": "https://anaconda.org/anaconda/anaconda-enterprise-cli", "description": "CLI tool for working with the Anaconda Enterprise DSP repository" }, + { "name": "anaconda-ident", "uri": "https://anaconda.org/anaconda/anaconda-ident", "description": "simple, opt-in user identification for conda clients" }, + { "name": "anaconda-linter", "uri": "https://anaconda.org/anaconda/anaconda-linter", "description": "A conda feedstock linter written in pure Python." }, + { "name": "anaconda-mirror", "uri": "https://anaconda.org/anaconda/anaconda-mirror", "description": "The official mirroring tool for Anaconda package repositories" }, + { "name": "anaconda-navigator", "uri": "https://anaconda.org/anaconda/anaconda-navigator", "description": "Anaconda Navigator" }, + { "name": "anaconda-oss-docs", "uri": "https://anaconda.org/anaconda/anaconda-oss-docs", "description": "No Summary" }, + { "name": "anaconda-project", "uri": "https://anaconda.org/anaconda/anaconda-project", "description": "Tool for encapsulating, running, and reproducing data science projects" }, + { "name": "anaconda-toolbox", "uri": "https://anaconda.org/anaconda/anaconda-toolbox", "description": "Anaconda Assistant: JupyterLab supercharged with a suite of Anaconda extensions, starting with the Anaconda Assistant AI chatbot." }, + { "name": "anaconda_powershell_prompt", "uri": "https://anaconda.org/anaconda/anaconda_powershell_prompt", "description": "PowerShell shortcut creator for Anaconda" }, + { "name": "anaconda_prompt", "uri": "https://anaconda.org/anaconda/anaconda_prompt", "description": "Terminal shortcut creator for Anaconda" }, + { "name": "aniso8601", "uri": "https://anaconda.org/anaconda/aniso8601", "description": "A library for parsing ISO 8601 strings." }, + { "name": "annotated-types", "uri": "https://anaconda.org/anaconda/annotated-types", "description": "Reusable constraint types to use with typing.Annotated" }, + { "name": "ansi2html", "uri": "https://anaconda.org/anaconda/ansi2html", "description": "Convert text with ANSI color codes to HTML or to LaTeX." }, + { "name": "ansicon", "uri": "https://anaconda.org/anaconda/ansicon", "description": "Python wrapper for loading Jason Hood's ANSICON" }, + { "name": "ant", "uri": "https://anaconda.org/anaconda/ant", "description": "Java build tool" }, + { "name": "antlr4-python3-runtime", "uri": "https://anaconda.org/anaconda/antlr4-python3-runtime", "description": "Python runtime for ANTLR." }, + { "name": "anyio", "uri": "https://anaconda.org/anaconda/anyio", "description": "High level compatibility layer for multiple asynchronous event loop implementations on Python" }, + { "name": "anyjson", "uri": "https://anaconda.org/anaconda/anyjson", "description": "Wraps the best available JSON implementation available in a common interface" }, + { "name": "anyqt", "uri": "https://anaconda.org/anaconda/anyqt", "description": "PyQt5/PyQt6 compatibility layer." }, + { "name": "aom", "uri": "https://anaconda.org/anaconda/aom", "description": "Alliance for Open Media video codec" }, + { "name": "apache-airflow", "uri": "https://anaconda.org/anaconda/apache-airflow", "description": "Airflow is a platform to programmatically author, schedule and monitor\nworkflows" }, + { "name": "apache-airflow-providers-apache-hdfs", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-apache-hdfs", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-apache-hdfs package" }, + { "name": "apache-airflow-providers-common-sql", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-common-sql", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-common-sql package" }, + { "name": "apache-airflow-providers-ftp", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-ftp", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-ftp package" }, + { "name": "apache-airflow-providers-http", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-http", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-http package" }, + { "name": "apache-airflow-providers-imap", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-imap", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-imap package" }, + { "name": "apache-airflow-providers-sqlite", "uri": "https://anaconda.org/anaconda/apache-airflow-providers-sqlite", "description": "Provider for Apache Airflow. Implements apache-airflow-providers-sqlite package" }, + { "name": "apache-libcloud", "uri": "https://anaconda.org/anaconda/apache-libcloud", "description": "Python library for interacting with many of the popular cloud service providers using a unified API" }, + { "name": "apipkg", "uri": "https://anaconda.org/anaconda/apipkg", "description": "With apipkg you can control the exported namespace of a python package and greatly reduce the number of imports for your users." }, + { "name": "apispec", "uri": "https://anaconda.org/anaconda/apispec", "description": "A pluggable API specification generator" }, + { "name": "applaunchservices", "uri": "https://anaconda.org/anaconda/applaunchservices", "description": "Simple package for registering an app with apple Launch Services to handle UTI and URL" }, + { "name": "appnope", "uri": "https://anaconda.org/anaconda/appnope", "description": "Disable App Nap on OS X 10.9" }, + { "name": "appscript", "uri": "https://anaconda.org/anaconda/appscript", "description": "Control AppleScriptable applications from Python." }, + { "name": "apscheduler", "uri": "https://anaconda.org/anaconda/apscheduler", "description": "In-process task scheduler with Cron-like capabilities" }, + { "name": "archspec", "uri": "https://anaconda.org/anaconda/archspec", "description": "A library to query system architecture" }, + { "name": "aredis", "uri": "https://anaconda.org/anaconda/aredis", "description": "Python async client for Redis key-value store" }, + { "name": "argh", "uri": "https://anaconda.org/anaconda/argh", "description": "An unobtrusive argparse wrapper with natural syntax" }, + { "name": "argon2-cffi", "uri": "https://anaconda.org/anaconda/argon2-cffi", "description": "The secure Argon2 password hashing algorithm." }, + { "name": "argon2-cffi-bindings", "uri": "https://anaconda.org/anaconda/argon2-cffi-bindings", "description": "Low-level Python CFFI Bindings for Argon2" }, + { "name": "argon2_cffi", "uri": "https://anaconda.org/anaconda/argon2_cffi", "description": "The secure Argon2 password hashing algorithm." }, + { "name": "armpl", "uri": "https://anaconda.org/anaconda/armpl", "description": "Free Arm Performance Libraries" }, + { "name": "arpack", "uri": "https://anaconda.org/anaconda/arpack", "description": "Fortran77 subroutines designed to solve large scale eigenvalue problems" }, + { "name": "array-api-strict", "uri": "https://anaconda.org/anaconda/array-api-strict", "description": "A strict, minimal implementation of the Python array API standard." }, + { "name": "arrow", "uri": "https://anaconda.org/anaconda/arrow", "description": "Better dates & times for Python" }, + { "name": "arrow-cpp", "uri": "https://anaconda.org/anaconda/arrow-cpp", "description": "C++ libraries for Apache Arrow" }, + { "name": "arviz", "uri": "https://anaconda.org/anaconda/arviz", "description": "Exploratory analysis of Bayesian models with Python" }, + { "name": "asciitree", "uri": "https://anaconda.org/anaconda/asciitree", "description": "Draws ASCII trees." }, + { "name": "asgiref", "uri": "https://anaconda.org/anaconda/asgiref", "description": "ASGI in-memory channel layer" }, + { "name": "asn1crypto", "uri": "https://anaconda.org/anaconda/asn1crypto", "description": "Python ASN.1 library with a focus on performance and a pythonic API" }, + { "name": "assimp", "uri": "https://anaconda.org/anaconda/assimp", "description": "A library to import and export various 3d-model-formats including scene-post-processing to generate missing render data." }, + { "name": "astor", "uri": "https://anaconda.org/anaconda/astor", "description": "Read, rewrite, and write Python ASTs nicely" }, + { "name": "astroid", "uri": "https://anaconda.org/anaconda/astroid", "description": "A abstract syntax tree for Python with inference support." }, + { "name": "astropy", "uri": "https://anaconda.org/anaconda/astropy", "description": "Community-developed Python Library for Astronomy" }, + { "name": "astropy-iers-data", "uri": "https://anaconda.org/anaconda/astropy-iers-data", "description": "IERS Earth Rotation and Leap Second tables for the astropy core package" }, + { "name": "asttokens", "uri": "https://anaconda.org/anaconda/asttokens", "description": "The asttokens module annotates Python abstract syntax trees (ASTs) with the positions of tokens and text in the source code that generated them." }, + { "name": "astunparse", "uri": "https://anaconda.org/anaconda/astunparse", "description": "An AST unparser for Python." }, + { "name": "asv", "uri": "https://anaconda.org/anaconda/asv", "description": "A simple Python benchmarking tool with web-based reporting" }, + { "name": "async-lru", "uri": "https://anaconda.org/anaconda/async-lru", "description": "Simple lru_cache for asyncio" }, + { "name": "async-timeout", "uri": "https://anaconda.org/anaconda/async-timeout", "description": "Timeout context manager for asyncio programs" }, + { "name": "async_generator", "uri": "https://anaconda.org/anaconda/async_generator", "description": "Async generators and context managers for Python 3.5+" }, + { "name": "asynch", "uri": "https://anaconda.org/anaconda/asynch", "description": "An asyncio ClickHouse Python Driver with native (TCP) interface support." }, + { "name": "asyncpg", "uri": "https://anaconda.org/anaconda/asyncpg", "description": "A fast PostgreSQL Database Client Library for Python/asyncio." }, + { "name": "asyncpgsa", "uri": "https://anaconda.org/anaconda/asyncpgsa", "description": "A fast PostgreSQL Database Client Library for Python/asyncio." }, + { "name": "asyncssh", "uri": "https://anaconda.org/anaconda/asyncssh", "description": "Asynchronous SSHv2 client and server library" }, + { "name": "asynctest", "uri": "https://anaconda.org/anaconda/asynctest", "description": "Enhance the standard unittest package with features for testing asyncio libraries" }, + { "name": "at-spi2-atk", "uri": "https://anaconda.org/anaconda/at-spi2-atk", "description": "bridge for AT-SPI and ATK accessibility technologies" }, + { "name": "at-spi2-core", "uri": "https://anaconda.org/anaconda/at-spi2-core", "description": "D-Bus-based implementation of AT-SPI accessibility framework" }, + { "name": "atk", "uri": "https://anaconda.org/anaconda/atk", "description": "Accessibility Toolkit." }, + { "name": "atk-1.0", "uri": "https://anaconda.org/anaconda/atk-1.0", "description": "Accessibility Toolkit." }, + { "name": "atk-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/atk-amzn2-aarch64", "description": "(CDT) Interfaces for accessibility support" }, + { "name": "atk-cos6-i686", "uri": "https://anaconda.org/anaconda/atk-cos6-i686", "description": "(CDT) Interfaces for accessibility support" }, + { "name": "atk-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/atk-cos7-ppc64le", "description": "(CDT) Interfaces for accessibility support" }, + { "name": "atk-cos7-s390x", "uri": "https://anaconda.org/anaconda/atk-cos7-s390x", "description": "(CDT) Interfaces for accessibility support" }, + { "name": "atk-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/atk-devel-amzn2-aarch64", "description": "(CDT) Development files for the ATK accessibility toolkit" }, + { "name": "atk-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/atk-devel-cos7-ppc64le", "description": "(CDT) Development files for the ATK accessibility toolkit" }, + { "name": "atk-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/atk-devel-cos7-s390x", "description": "(CDT) Development files for the ATK accessibility toolkit" }, + { "name": "atkmm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/atkmm-amzn2-aarch64", "description": "(CDT) C++ interface for the ATK library" }, + { "name": "atkmm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/atkmm-devel-amzn2-aarch64", "description": "(CDT) Development files for atkmm" }, + { "name": "atlasclient", "uri": "https://anaconda.org/anaconda/atlasclient", "description": "Apache Atlas client in Python" }, + { "name": "atom", "uri": "https://anaconda.org/anaconda/atom", "description": "Memory efficient Python objects" }, + { "name": "atomicwrites", "uri": "https://anaconda.org/anaconda/atomicwrites", "description": "Atomic file writes" }, + { "name": "attrs", "uri": "https://anaconda.org/anaconda/attrs", "description": "attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka dunder methods)." }, + { "name": "audit-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/audit-libs-amzn2-aarch64", "description": "(CDT) Dynamic library for libaudit" }, + { "name": "audit-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/audit-libs-cos6-i686", "description": "(CDT) Dynamic library for libaudit" }, + { "name": "audit-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/audit-libs-cos6-x86_64", "description": "(CDT) Dynamic library for libaudit" }, + { "name": "authlib", "uri": "https://anaconda.org/anaconda/authlib", "description": "The ultimate Python library in building OAuth and OpenID Connect servers. JWS,JWE,JWK,JWA,JWT included. https://authlib.org/" }, + { "name": "autocfg", "uri": "https://anaconda.org/anaconda/autocfg", "description": "All you need is a minimal config system for automl." }, + { "name": "autoconf-archive", "uri": "https://anaconda.org/anaconda/autoconf-archive", "description": "Collection of over 500 reusable autoconf macros" }, + { "name": "autograd", "uri": "https://anaconda.org/anaconda/autograd", "description": "Efficiently computes derivatives of numpy code." }, + { "name": "autograd-gamma", "uri": "https://anaconda.org/anaconda/autograd-gamma", "description": "autograd compatible approximations to the derivatives of the Gamma-family of functions." }, + { "name": "automat", "uri": "https://anaconda.org/anaconda/automat", "description": "self-service finite-state machines for the programmer on the go" }, + { "name": "autopep8", "uri": "https://anaconda.org/anaconda/autopep8", "description": "A tool that automatically formats Python code to conform to the PEP 8 style guide" }, + { "name": "autotools_clang_conda", "uri": "https://anaconda.org/anaconda/autotools_clang_conda", "description": "Scripts to compile autotools projects on windows using clang and llvm tools" }, + { "name": "autovizwidget", "uri": "https://anaconda.org/anaconda/autovizwidget", "description": "AutoVizWidget: An Auto-Visualization library for pandas dataframes" }, + { "name": "avahi-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/avahi-libs-amzn2-aarch64", "description": "(CDT) Libraries for avahi run-time use" }, + { "name": "avahi-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/avahi-libs-cos6-i686", "description": "(CDT) Libraries for avahi run-time use" }, + { "name": "avahi-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/avahi-libs-cos6-x86_64", "description": "(CDT) Libraries for avahi run-time use" }, + { "name": "avahi-libs-cos7-s390x", "uri": "https://anaconda.org/anaconda/avahi-libs-cos7-s390x", "description": "(CDT) Libraries for avahi run-time use" }, + { "name": "aws-c-auth", "uri": "https://anaconda.org/anaconda/aws-c-auth", "description": "C99 library implementation of AWS client-side authentication: standard credentials providers and signing." }, + { "name": "aws-c-cal", "uri": "https://anaconda.org/anaconda/aws-c-cal", "description": "Aws Crypto Abstraction Layer" }, + { "name": "aws-c-common", "uri": "https://anaconda.org/anaconda/aws-c-common", "description": "Core c99 package for AWS SDK for C. Includes cross-platform primitives, configuration, data structures, and error handling." }, + { "name": "aws-c-compression", "uri": "https://anaconda.org/anaconda/aws-c-compression", "description": "C99 implementation of huffman encoding/decoding" }, + { "name": "aws-c-event-stream", "uri": "https://anaconda.org/anaconda/aws-c-event-stream", "description": "C99 implementation of the vnd.amazon.eventstream content-type." }, + { "name": "aws-c-http", "uri": "https://anaconda.org/anaconda/aws-c-http", "description": "C99 implementation of the HTTP protocol" }, + { "name": "aws-c-io", "uri": "https://anaconda.org/anaconda/aws-c-io", "description": "This is a module for the AWS SDK for C. It handles all IO and TLS work for application protocols." }, + { "name": "aws-c-mqtt", "uri": "https://anaconda.org/anaconda/aws-c-mqtt", "description": "C99 implementation of the MQTT" }, + { "name": "aws-c-s3", "uri": "https://anaconda.org/anaconda/aws-c-s3", "description": "C99 library implementation for communicating with the S3 service." }, + { "name": "aws-c-sdkutils", "uri": "https://anaconda.org/anaconda/aws-c-sdkutils", "description": "This is a module for the AWS SDK for C." }, + { "name": "aws-checksums", "uri": "https://anaconda.org/anaconda/aws-checksums", "description": "Cross-Platform HW accelerated CRC32c and CRC32 with fallback to efficient SW implementations. C interface with language bindings for each of our SDKs." }, + { "name": "aws-crt-cpp", "uri": "https://anaconda.org/anaconda/aws-crt-cpp", "description": "C++ wrapper around the aws-c-* libraries." }, + { "name": "aws-requests-auth", "uri": "https://anaconda.org/anaconda/aws-requests-auth", "description": "AWS signature version 4 signing process for the python requests module" }, + { "name": "aws-sam-translator", "uri": "https://anaconda.org/anaconda/aws-sam-translator", "description": "AWS Serverless Application Model (AWS SAM) prescribes rules for expressing Serverless applications on AWS." }, + { "name": "aws-sdk-cpp", "uri": "https://anaconda.org/anaconda/aws-sdk-cpp", "description": "C++ library that makes it easy to integrate C++ applications with AWS services" }, + { "name": "aws-xray-sdk", "uri": "https://anaconda.org/anaconda/aws-xray-sdk", "description": "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." }, + { "name": "azure-common", "uri": "https://anaconda.org/anaconda/azure-common", "description": "Microsoft Azure Client Library for Python (Common)" }, + { "name": "azure-core", "uri": "https://anaconda.org/anaconda/azure-core", "description": "Microsoft Azure Core Library for Python" }, + { "name": "azure-cosmos", "uri": "https://anaconda.org/anaconda/azure-cosmos", "description": "Microsoft Azure Cosmos Python SDK" }, + { "name": "azure-functions", "uri": "https://anaconda.org/anaconda/azure-functions", "description": "Azure Functions for Python" }, + { "name": "azure-storage-blob", "uri": "https://anaconda.org/anaconda/azure-storage-blob", "description": "Microsoft Azure Blob Storage Client Library for Python" }, + { "name": "babel", "uri": "https://anaconda.org/anaconda/babel", "description": "Utilities to internationalize and localize Python applications" }, + { "name": "backcall", "uri": "https://anaconda.org/anaconda/backcall", "description": "Specifications for callback functions passed in to an API" }, + { "name": "backoff", "uri": "https://anaconda.org/anaconda/backoff", "description": "Function decoration for backoff and retry" }, + { "name": "backports", "uri": "https://anaconda.org/anaconda/backports", "description": "Namespace for backported Python features." }, + { "name": "backports.lzma", "uri": "https://anaconda.org/anaconda/backports.lzma", "description": "Backport of Python 3.3's 'lzma' module for XZ/LZMA compressed files." }, + { "name": "backports.os", "uri": "https://anaconda.org/anaconda/backports.os", "description": "Backport of new features in Python's os module" }, + { "name": "backports.shutil_which", "uri": "https://anaconda.org/anaconda/backports.shutil_which", "description": "Backport of shutil.which from Python 3.3" }, + { "name": "backports.tarfile", "uri": "https://anaconda.org/anaconda/backports.tarfile", "description": "Backport of CPython tarfile module" }, + { "name": "backports.tempfile", "uri": "https://anaconda.org/anaconda/backports.tempfile", "description": "Backports of new features in Python's tempfile module" }, + { "name": "backports.weakref", "uri": "https://anaconda.org/anaconda/backports.weakref", "description": "Backport of new features in Python's weakref module" }, + { "name": "backports.zoneinfo", "uri": "https://anaconda.org/anaconda/backports.zoneinfo", "description": "Backport of the standard library zoneinfo module" }, + { "name": "basemap", "uri": "https://anaconda.org/anaconda/basemap", "description": "Plot on map projections using matplotlib" }, + { "name": "basemap-data", "uri": "https://anaconda.org/anaconda/basemap-data", "description": "Plot on map projections (with coastlines and political boundaries) using matplotlib." }, + { "name": "basemap-data-hires", "uri": "https://anaconda.org/anaconda/basemap-data-hires", "description": "Plot on map projections (with coastlines and political boundaries) using matplotlib." }, + { "name": "basesystem-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/basesystem-amzn2-aarch64", "description": "(CDT) The skeleton package which defines a simple Amazon Linux system" }, + { "name": "bash", "uri": "https://anaconda.org/anaconda/bash", "description": "Bourne Again Shell" }, + { "name": "baycomp", "uri": "https://anaconda.org/anaconda/baycomp", "description": "Library for Bayesian comparison of predictive models" }, + { "name": "bazel", "uri": "https://anaconda.org/anaconda/bazel", "description": "build system originally authored by Google" }, + { "name": "bazel-toolchain", "uri": "https://anaconda.org/anaconda/bazel-toolchain", "description": "Helper script to generate a crosscompile toolchain for Bazel with the currently activated compiler settings." }, + { "name": "bcj-cffi", "uri": "https://anaconda.org/anaconda/bcj-cffi", "description": "bcj algorithm library" }, + { "name": "bcrypt", "uri": "https://anaconda.org/anaconda/bcrypt", "description": "Modern password hashing for your software and your servers" }, + { "name": "beautifulsoup4", "uri": "https://anaconda.org/anaconda/beautifulsoup4", "description": "Python library designed for screen-scraping" }, + { "name": "behave", "uri": "https://anaconda.org/anaconda/behave", "description": "behave is behaviour-driven development, Python style" }, + { "name": "beniget", "uri": "https://anaconda.org/anaconda/beniget", "description": "Extract semantic information about static Python code" }, + { "name": "bidict", "uri": "https://anaconda.org/anaconda/bidict", "description": "Efficient, Pythonic bidirectional map implementation and related functionality" }, + { "name": "billiard", "uri": "https://anaconda.org/anaconda/billiard", "description": "Python multiprocessing fork with improvements and bugfixes" }, + { "name": "binaryornot", "uri": "https://anaconda.org/anaconda/binaryornot", "description": "Ultra-lightweight pure Python package to check if a file is binary or text." }, + { "name": "binsort", "uri": "https://anaconda.org/anaconda/binsort", "description": "Binsort - sort files by binary similarity" }, + { "name": "binutils", "uri": "https://anaconda.org/anaconda/binutils", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "binutils_impl_linux-32", "uri": "https://anaconda.org/anaconda/binutils_impl_linux-32", "description": "The GNU Binutils are a collection of binary tools." }, + { "name": "binutils_impl_linux-64", "uri": "https://anaconda.org/anaconda/binutils_impl_linux-64", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "binutils_impl_linux-aarch64", "uri": "https://anaconda.org/anaconda/binutils_impl_linux-aarch64", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "binutils_impl_linux-ppc64le", "uri": "https://anaconda.org/anaconda/binutils_impl_linux-ppc64le", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "binutils_impl_linux-s390x", "uri": "https://anaconda.org/anaconda/binutils_impl_linux-s390x", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "binutils_linux-32", "uri": "https://anaconda.org/anaconda/binutils_linux-32", "description": "The GNU Binutils are a collection of binary tools (activation scripts)" }, + { "name": "binutils_linux-64", "uri": "https://anaconda.org/anaconda/binutils_linux-64", "description": "The GNU Binutils are a collection of binary tools (activation scripts)" }, + { "name": "binutils_linux-aarch64", "uri": "https://anaconda.org/anaconda/binutils_linux-aarch64", "description": "The GNU Binutils are a collection of binary tools (activation scripts)" }, + { "name": "binutils_linux-ppc64le", "uri": "https://anaconda.org/anaconda/binutils_linux-ppc64le", "description": "The GNU Binutils are a collection of binary tools (activation scripts)" }, + { "name": "binutils_linux-s390x", "uri": "https://anaconda.org/anaconda/binutils_linux-s390x", "description": "The GNU Binutils are a collection of binary tools (activation scripts)" }, + { "name": "biopython", "uri": "https://anaconda.org/anaconda/biopython", "description": "Collection of freely available tools for computational molecular biology" }, + { "name": "bisonpp", "uri": "https://anaconda.org/anaconda/bisonpp", "description": "The original bison++ project, brought up to date with modern compilers" }, + { "name": "bitarray", "uri": "https://anaconda.org/anaconda/bitarray", "description": "efficient arrays of booleans -- C extension" }, + { "name": "bkcharts", "uri": "https://anaconda.org/anaconda/bkcharts", "description": "No Summary" }, + { "name": "black", "uri": "https://anaconda.org/anaconda/black", "description": "The Uncompromising Code Formatter" }, + { "name": "blas", "uri": "https://anaconda.org/anaconda/blas", "description": "Linear Algebra PACKage" }, + { "name": "blas-devel", "uri": "https://anaconda.org/anaconda/blas-devel", "description": "Linear Algebra PACKage" }, + { "name": "bleach", "uri": "https://anaconda.org/anaconda/bleach", "description": "Easy, whitelist-based HTML-sanitizing tool" }, + { "name": "blessed", "uri": "https://anaconda.org/anaconda/blessed", "description": "Easy, practical library for making terminal apps, by providing an elegant, well-documented interface to Colors, Keyboard input, and screen Positioning capabilities." }, + { "name": "blessings", "uri": "https://anaconda.org/anaconda/blessings", "description": "A thin, practical wrapper around terminal capabilities in Python" }, + { "name": "blinker", "uri": "https://anaconda.org/anaconda/blinker", "description": "Fast, simple object-to-object and broadcast signaling" }, + { "name": "bokeh", "uri": "https://anaconda.org/anaconda/bokeh", "description": "Bokeh is an interactive visualization library for modern web browsers." }, + { "name": "boltons", "uri": "https://anaconda.org/anaconda/boltons", "description": "boltons should be builtins. Boltons is a set of over 160 BSD-licensed, pure-Python utilities in the same spirit as--and yet conspicuously missing from--the standard library." }, + { "name": "boolean.py", "uri": "https://anaconda.org/anaconda/boolean.py", "description": "Define boolean algebras, create and parse boolean expressions and create custom boolean DSL." }, + { "name": "boost", "uri": "https://anaconda.org/anaconda/boost", "description": "Free peer-reviewed portable C++ source libraries." }, + { "name": "boost-cpp", "uri": "https://anaconda.org/anaconda/boost-cpp", "description": "Free peer-reviewed portable C++ source libraries." }, + { "name": "boost_mp11", "uri": "https://anaconda.org/anaconda/boost_mp11", "description": "C++11 metaprogramming library" }, + { "name": "boto3", "uri": "https://anaconda.org/anaconda/boto3", "description": "Amazon Web Services SDK for Python" }, + { "name": "botocore", "uri": "https://anaconda.org/anaconda/botocore", "description": "Low-level, data-driven core of boto 3." }, + { "name": "bottle", "uri": "https://anaconda.org/anaconda/bottle", "description": "Bottle is a fast, simple and lightweight WSGI micro web-framework for Python." }, + { "name": "bottleneck", "uri": "https://anaconda.org/anaconda/bottleneck", "description": "Fast NumPy array functions written in Cython." }, + { "name": "bpython", "uri": "https://anaconda.org/anaconda/bpython", "description": "Fancy Interface to the Python Interpreter" }, + { "name": "branca", "uri": "https://anaconda.org/anaconda/branca", "description": "This library is a spinoff from folium with the non-map-specific features" }, + { "name": "brotli", "uri": "https://anaconda.org/anaconda/brotli", "description": "Brotli compression format" }, + { "name": "brotli-bin", "uri": "https://anaconda.org/anaconda/brotli-bin", "description": "Brotli compression format" }, + { "name": "brotli-python", "uri": "https://anaconda.org/anaconda/brotli-python", "description": "Brotli compression format" }, + { "name": "brotlicffi", "uri": "https://anaconda.org/anaconda/brotlicffi", "description": "Python CFFI bindings to the Brotli library" }, + { "name": "brotlipy", "uri": "https://anaconda.org/anaconda/brotlipy", "description": "Python bindings to the Brotli compression library" }, + { "name": "brunsli", "uri": "https://anaconda.org/anaconda/brunsli", "description": "Practical JPEG Repacker" }, + { "name": "bs4", "uri": "https://anaconda.org/anaconda/bs4", "description": "Python library designed for screen-scraping" }, + { "name": "bsdiff4", "uri": "https://anaconda.org/anaconda/bsdiff4", "description": "binary diff and patch using the BSDIFF4-format" }, + { "name": "btrees", "uri": "https://anaconda.org/anaconda/btrees", "description": "scalable persistent object containers" }, + { "name": "build", "uri": "https://anaconda.org/anaconda/build", "description": "A simple, correct PEP517 package builder" }, + { "name": "bwidget", "uri": "https://anaconda.org/anaconda/bwidget", "description": "Simple Widgets for Tcl/Tk." }, + { "name": "bytecode", "uri": "https://anaconda.org/anaconda/bytecode", "description": "A Python module to generate and modify Python bytecode." }, + { "name": "bzip2", "uri": "https://anaconda.org/anaconda/bzip2", "description": "high-quality data compressor" }, + { "name": "bzip2-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/bzip2-libs-amzn2-aarch64", "description": "(CDT) Libraries for applications using bzip2" }, + { "name": "c-ares", "uri": "https://anaconda.org/anaconda/c-ares", "description": "This is c-ares, an asynchronous resolver library" }, + { "name": "c-ares-static", "uri": "https://anaconda.org/anaconda/c-ares-static", "description": "This is c-ares, an asynchronous resolver library" }, + { "name": "c-blosc2", "uri": "https://anaconda.org/anaconda/c-blosc2", "description": "A simple, compressed, fast and persistent data store library for C" }, + { "name": "c99-to-c89", "uri": "https://anaconda.org/anaconda/c99-to-c89", "description": "Tool to convert C99 code to MSVC-compatible C89, with many Anaconda Distribution fixes" }, + { "name": "ca-certificates", "uri": "https://anaconda.org/anaconda/ca-certificates", "description": "Certificates for use with other packages." }, + { "name": "ca-certificates-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/ca-certificates-amzn2-aarch64", "description": "(CDT) The Mozilla CA root certificate bundle" }, + { "name": "ca-certificates-cos6-x86_64", "uri": "https://anaconda.org/anaconda/ca-certificates-cos6-x86_64", "description": "(CDT) The Mozilla CA root certificate bundle" }, + { "name": "ca-certificates-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/ca-certificates-cos7-ppc64le", "description": "(CDT) The Mozilla CA root certificate bundle" }, + { "name": "ca-certificates-cos7-s390x", "uri": "https://anaconda.org/anaconda/ca-certificates-cos7-s390x", "description": "(CDT) The Mozilla CA root certificate bundle" }, + { "name": "cachecontrol", "uri": "https://anaconda.org/anaconda/cachecontrol", "description": "The httplib2 caching algorithms packaged up for use with requests" }, + { "name": "cachecontrol-with-filecache", "uri": "https://anaconda.org/anaconda/cachecontrol-with-filecache", "description": "The httplib2 caching algorithms packaged up for use with requests" }, + { "name": "cachecontrol-with-redis", "uri": "https://anaconda.org/anaconda/cachecontrol-with-redis", "description": "The httplib2 caching algorithms packaged up for use with requests" }, + { "name": "cachelib", "uri": "https://anaconda.org/anaconda/cachelib", "description": "A collection of cache libraries in the same API interface." }, + { "name": "cachetools", "uri": "https://anaconda.org/anaconda/cachetools", "description": "Extensible memoizing collections and decorators" }, + { "name": "cachy", "uri": "https://anaconda.org/anaconda/cachy", "description": "Cachy provides a simple yet effective caching library" }, + { "name": "cairo", "uri": "https://anaconda.org/anaconda/cairo", "description": "Cairo is a 2D graphics library with support for multiple output devices." }, + { "name": "cairo-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cairo-amzn2-aarch64", "description": "(CDT) A 2D graphics library" }, + { "name": "cairo-cos6-i686", "uri": "https://anaconda.org/anaconda/cairo-cos6-i686", "description": "(CDT) A 2D graphics library" }, + { "name": "cairo-cos6-x86_64", "uri": "https://anaconda.org/anaconda/cairo-cos6-x86_64", "description": "(CDT) A 2D graphics library" }, + { "name": "cairo-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/cairo-cos7-ppc64le", "description": "(CDT) A 2D graphics library" }, + { "name": "cairo-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cairo-devel-amzn2-aarch64", "description": "(CDT) Development files for cairo" }, + { "name": "cairo-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/cairo-devel-cos7-ppc64le", "description": "(CDT) Development files for cairo" }, + { "name": "cairo-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/cairo-devel-cos7-s390x", "description": "(CDT) Development files for cairo" }, + { "name": "cairomm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cairomm-amzn2-aarch64", "description": "(CDT) C++ API for the cairo graphics library" }, + { "name": "cairomm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cairomm-devel-amzn2-aarch64", "description": "(CDT) Headers for developing programs that will use cairomm" }, + { "name": "calver", "uri": "https://anaconda.org/anaconda/calver", "description": "Setuptools extension for CalVer package versions" }, + { "name": "canmatrix", "uri": "https://anaconda.org/anaconda/canmatrix", "description": "Converting Can (Controller Area Network) Database Formats .arxml .dbc .dbf .kcd ..." }, + { "name": "capnproto", "uri": "https://anaconda.org/anaconda/capnproto", "description": "An insanely fast data interchange format and capability-based RPC system." }, + { "name": "captum", "uri": "https://anaconda.org/anaconda/captum", "description": "Model interpretability for PyTorch" }, + { "name": "capturer", "uri": "https://anaconda.org/anaconda/capturer", "description": "Easily capture stdout/stderr of the current process and subprocesses." }, + { "name": "cargo-bundle-licenses", "uri": "https://anaconda.org/anaconda/cargo-bundle-licenses", "description": "Bundle thirdparty licenses for Cargo projects into a single file." }, + { "name": "cargo-bundle-licenses-gnu", "uri": "https://anaconda.org/anaconda/cargo-bundle-licenses-gnu", "description": "Bundle thirdparty licenses for Cargo projects into a single file." }, + { "name": "cartopy", "uri": "https://anaconda.org/anaconda/cartopy", "description": "A library providing cartographic tools for python" }, + { "name": "case", "uri": "https://anaconda.org/anaconda/case", "description": "Python unittest Utilities" }, + { "name": "cassandra-driver", "uri": "https://anaconda.org/anaconda/cassandra-driver", "description": "Python driver for Cassandra" }, + { "name": "catalogue", "uri": "https://anaconda.org/anaconda/catalogue", "description": "Super lightweight function registries for your library" }, + { "name": "catboost", "uri": "https://anaconda.org/anaconda/catboost", "description": "Gradient boosting on decision trees library" }, + { "name": "category_encoders", "uri": "https://anaconda.org/anaconda/category_encoders", "description": "A collection sklearn transformers to encode categorical variables as numeric" }, + { "name": "cattrs", "uri": "https://anaconda.org/anaconda/cattrs", "description": "Complex custom class converters for attrs." }, + { "name": "ccache", "uri": "https://anaconda.org/anaconda/ccache", "description": "A compiler cache" }, + { "name": "cccl", "uri": "https://anaconda.org/anaconda/cccl", "description": "CUDA C++ Core Libraries" }, + { "name": "cchardet", "uri": "https://anaconda.org/anaconda/cchardet", "description": "cChardet is high speed universal character encoding detector" }, + { "name": "cctools", "uri": "https://anaconda.org/anaconda/cctools", "description": "Native assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "cctools_linux-64", "uri": "https://anaconda.org/anaconda/cctools_linux-64", "description": "Assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "cctools_linux-aarch64", "uri": "https://anaconda.org/anaconda/cctools_linux-aarch64", "description": "Assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "cctools_linux-ppc64le", "uri": "https://anaconda.org/anaconda/cctools_linux-ppc64le", "description": "Assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "cctools_osx-64", "uri": "https://anaconda.org/anaconda/cctools_osx-64", "description": "Assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "cctools_osx-arm64", "uri": "https://anaconda.org/anaconda/cctools_osx-arm64", "description": "Assembler, archiver, ranlib, libtool, otool et al for Darwin Mach-O files" }, + { "name": "celery", "uri": "https://anaconda.org/anaconda/celery", "description": "Distributed Task Queue" }, + { "name": "celery-redbeat", "uri": "https://anaconda.org/anaconda/celery-redbeat", "description": "A Celery Beat Scheduler using Redis for persistent storage" }, + { "name": "centos-release-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/centos-release-cos7-ppc64le", "description": "(CDT) CentOS Linux release file" }, + { "name": "cerberus", "uri": "https://anaconda.org/anaconda/cerberus", "description": "Lightweight, extensible schema and data validation tool for Python dictionaries." }, + { "name": "ceres-solver", "uri": "https://anaconda.org/anaconda/ceres-solver", "description": "A large scale non-linear optimization library" }, + { "name": "certifi", "uri": "https://anaconda.org/anaconda/certifi", "description": "Python package for providing Mozilla's CA Bundle." }, + { "name": "certipy", "uri": "https://anaconda.org/anaconda/certipy", "description": "Simple, fast, extensible JSON encoder/decoder for Python" }, + { "name": "cffi", "uri": "https://anaconda.org/anaconda/cffi", "description": "Foreign Function Interface for Python calling C code." }, + { "name": "cfgv", "uri": "https://anaconda.org/anaconda/cfgv", "description": "Validate configuration and produce human readable error messages." }, + { "name": "cfitsio", "uri": "https://anaconda.org/anaconda/cfitsio", "description": "A library for reading and writing FITS files" }, + { "name": "cfn-lint", "uri": "https://anaconda.org/anaconda/cfn-lint", "description": "CloudFormation Linter" }, + { "name": "cftime", "uri": "https://anaconda.org/anaconda/cftime", "description": "Time-handling functionality from netcdf4-python" }, + { "name": "cgroupspy", "uri": "https://anaconda.org/anaconda/cgroupspy", "description": "Python library for managing cgroups" }, + { "name": "chai", "uri": "https://anaconda.org/anaconda/chai", "description": "Easy to use mocking, stubbing and spying framework." }, + { "name": "chainer", "uri": "https://anaconda.org/anaconda/chainer", "description": "A flexible framework of neural networks" }, + { "name": "chardet", "uri": "https://anaconda.org/anaconda/chardet", "description": "Universal character encoding detector" }, + { "name": "charls", "uri": "https://anaconda.org/anaconda/charls", "description": "CharLS is a C++ implementation of the JPEG-LS standard for lossless and near-lossless image compression and decompression. JPEG-LS is a low-complexity image compression standard that matches JPEG 2000 compression ratios." }, + { "name": "charset-normalizer", "uri": "https://anaconda.org/anaconda/charset-normalizer", "description": "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." }, + { "name": "cheroot", "uri": "https://anaconda.org/anaconda/cheroot", "description": "Highly-optimized, pure-python HTTP server" }, + { "name": "cherrypy", "uri": "https://anaconda.org/anaconda/cherrypy", "description": "Object-Oriented HTTP framework" }, + { "name": "chex", "uri": "https://anaconda.org/anaconda/chex", "description": "Chex: Testing made fun, in JAX!" }, + { "name": "chkconfig-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/chkconfig-cos7-ppc64le", "description": "(CDT) A system tool for maintaining the /etc/rc*.d hierarchy" }, + { "name": "chkconfig-cos7-s390x", "uri": "https://anaconda.org/anaconda/chkconfig-cos7-s390x", "description": "(CDT) A system tool for maintaining the /etc/rc*.d hierarchy" }, + { "name": "chromedriver-binary", "uri": "https://anaconda.org/anaconda/chromedriver-binary", "description": "No Summary" }, + { "name": "ciocheck", "uri": "https://anaconda.org/anaconda/ciocheck", "description": "Continuum Analytics linter/formater/tester helper" }, + { "name": "ciso8601", "uri": "https://anaconda.org/anaconda/ciso8601", "description": "Fast ISO8601 date time parser for Python written in C" }, + { "name": "clang", "uri": "https://anaconda.org/anaconda/clang", "description": "Development headers and libraries for Clang" }, + { "name": "clang-12", "uri": "https://anaconda.org/anaconda/clang-12", "description": "Development headers and libraries for Clang" }, + { "name": "clang-14", "uri": "https://anaconda.org/anaconda/clang-14", "description": "Development headers and libraries for Clang" }, + { "name": "clang-dbg_osx-64", "uri": "https://anaconda.org/anaconda/clang-dbg_osx-64", "description": "No Summary" }, + { "name": "clang-format", "uri": "https://anaconda.org/anaconda/clang-format", "description": "Development headers and libraries for Clang" }, + { "name": "clang-format-14", "uri": "https://anaconda.org/anaconda/clang-format-14", "description": "Development headers and libraries for Clang" }, + { "name": "clang-tools", "uri": "https://anaconda.org/anaconda/clang-tools", "description": "Development headers and libraries for Clang" }, + { "name": "clang_bootstrap_osx-64", "uri": "https://anaconda.org/anaconda/clang_bootstrap_osx-64", "description": "clang compiler components in one package for bootstrapping clang" }, + { "name": "clang_bootstrap_osx-arm64", "uri": "https://anaconda.org/anaconda/clang_bootstrap_osx-arm64", "description": "clang compiler components in one package for bootstrapping clang" }, + { "name": "clang_osx-64", "uri": "https://anaconda.org/anaconda/clang_osx-64", "description": "clang compilers for conda-build 3" }, + { "name": "clang_osx-arm64", "uri": "https://anaconda.org/anaconda/clang_osx-arm64", "description": "clang compilers for conda-build 3" }, + { "name": "clang_win-64", "uri": "https://anaconda.org/anaconda/clang_win-64", "description": "clang (cross) compiler for windows with MSVC ABI compatbility" }, + { "name": "clangdev", "uri": "https://anaconda.org/anaconda/clangdev", "description": "Development headers and libraries for Clang" }, + { "name": "clangxx", "uri": "https://anaconda.org/anaconda/clangxx", "description": "Development headers and libraries for Clang" }, + { "name": "clangxx-dbg_osx-64", "uri": "https://anaconda.org/anaconda/clangxx-dbg_osx-64", "description": "No Summary" }, + { "name": "clangxx_osx-64", "uri": "https://anaconda.org/anaconda/clangxx_osx-64", "description": "clang compilers for conda-build 3" }, + { "name": "clangxx_osx-arm64", "uri": "https://anaconda.org/anaconda/clangxx_osx-arm64", "description": "clang compilers for conda-build 3" }, + { "name": "cleo", "uri": "https://anaconda.org/anaconda/cleo", "description": "Cleo allows you to create beautiful and testable command-line interfaces." }, + { "name": "cli11", "uri": "https://anaconda.org/anaconda/cli11", "description": "CLI11 is a command line parser for C++11 and beyond that provides a rich feature set with a simple and intuitive interface." }, + { "name": "click", "uri": "https://anaconda.org/anaconda/click", "description": "Python composable command line interface toolkit" }, + { "name": "click-default-group", "uri": "https://anaconda.org/anaconda/click-default-group", "description": "Extends click.Group to invoke a command without explicit subcommand name.'" }, + { "name": "click-didyoumean", "uri": "https://anaconda.org/anaconda/click-didyoumean", "description": "Enable git-like did-you-mean feature in click." }, + { "name": "click-repl", "uri": "https://anaconda.org/anaconda/click-repl", "description": "REPL plugin for Click" }, + { "name": "clickclick", "uri": "https://anaconda.org/anaconda/clickclick", "description": "Click utility functions" }, + { "name": "clickhouse-cityhash", "uri": "https://anaconda.org/anaconda/clickhouse-cityhash", "description": "Python-bindings for CityHash, a fast non-cryptographic hash algorithm" }, + { "name": "clickhouse-driver", "uri": "https://anaconda.org/anaconda/clickhouse-driver", "description": "Python driver with native interface for ClickHouse" }, + { "name": "clickhouse-sqlalchemy", "uri": "https://anaconda.org/anaconda/clickhouse-sqlalchemy", "description": "Simple ClickHouse SQLAlchemy Dialect" }, + { "name": "clikit", "uri": "https://anaconda.org/anaconda/clikit", "description": "CliKit is a group of utilities to build beautiful and testable command line interfaces." }, + { "name": "cloudant", "uri": "https://anaconda.org/anaconda/cloudant", "description": "Asynchronous Cloudant / CouchDB Interface" }, + { "name": "cloudpathlib", "uri": "https://anaconda.org/anaconda/cloudpathlib", "description": "pathlib.Path-style classes for interacting with files in different cloud storage services." }, + { "name": "cloudpathlib-all", "uri": "https://anaconda.org/anaconda/cloudpathlib-all", "description": "pathlib.Path-style classes for interacting with files in different cloud storage services." }, + { "name": "cloudpathlib-azure", "uri": "https://anaconda.org/anaconda/cloudpathlib-azure", "description": "pathlib.Path-style classes for interacting with files in different cloud storage services." }, + { "name": "cloudpathlib-gs", "uri": "https://anaconda.org/anaconda/cloudpathlib-gs", "description": "pathlib.Path-style classes for interacting with files in different cloud storage services." }, + { "name": "cloudpathlib-s3", "uri": "https://anaconda.org/anaconda/cloudpathlib-s3", "description": "pathlib.Path-style classes for interacting with files in different cloud storage services." }, + { "name": "cloudpickle", "uri": "https://anaconda.org/anaconda/cloudpickle", "description": "Extended pickling support for Python objects" }, + { "name": "clr_loader", "uri": "https://anaconda.org/anaconda/clr_loader", "description": "Generic pure Python loader for .NET runtimes" }, + { "name": "cmaes", "uri": "https://anaconda.org/anaconda/cmaes", "description": "Lightweight Covariance Matrix Adaptation Evolution Strategy (CMA-ES) implementation for Python 3." }, + { "name": "cmake", "uri": "https://anaconda.org/anaconda/cmake", "description": "CMake is an extensible, open-source system that manages the build process" }, + { "name": "cmake-binary", "uri": "https://anaconda.org/anaconda/cmake-binary", "description": "CMake is an extensible, open-source system that manages the build process" }, + { "name": "cmake-no-system", "uri": "https://anaconda.org/anaconda/cmake-no-system", "description": "CMake built without system libraries for use when building CMake dependencies." }, + { "name": "cmake_setuptools", "uri": "https://anaconda.org/anaconda/cmake_setuptools", "description": "Provides some usable cmake related build extensions" }, + { "name": "cmarkgfm", "uri": "https://anaconda.org/anaconda/cmarkgfm", "description": "Minimalist Python bindings to GitHub’s fork of cmark." }, + { "name": "cmdstan", "uri": "https://anaconda.org/anaconda/cmdstan", "description": "CmdStan, the command line interface to Stan" }, + { "name": "cmdstanpy", "uri": "https://anaconda.org/anaconda/cmdstanpy", "description": "CmdStanPy is a lightweight interface to Stan for Python users which\nprovides the necessary objects and functions to compile a Stan program\nand fit the model to data using CmdStan." }, + { "name": "cmyt", "uri": "https://anaconda.org/anaconda/cmyt", "description": "A collection of Matplotlib colormaps from the yt project" }, + { "name": "codecov", "uri": "https://anaconda.org/anaconda/codecov", "description": "Hosted coverage reports for Github, Bitbucket and Gitlab" }, + { "name": "coin-or-cbc", "uri": "https://anaconda.org/anaconda/coin-or-cbc", "description": "COIN-OR branch and cut (Cbc)" }, + { "name": "coin-or-cgl", "uri": "https://anaconda.org/anaconda/coin-or-cgl", "description": "COIN-OR Cut Generation Library (Cgl)" }, + { "name": "coin-or-clp", "uri": "https://anaconda.org/anaconda/coin-or-clp", "description": "COIN-OR linear programming (Clp)" }, + { "name": "coin-or-osi", "uri": "https://anaconda.org/anaconda/coin-or-osi", "description": "Coin OR Open Solver Interface (OSI)" }, + { "name": "coin-or-utils", "uri": "https://anaconda.org/anaconda/coin-or-utils", "description": "COIN-OR Utilities (CoinUtils)" }, + { "name": "coincbc", "uri": "https://anaconda.org/anaconda/coincbc", "description": "COIN-OR branch and cut (Cbc)" }, + { "name": "colorama", "uri": "https://anaconda.org/anaconda/colorama", "description": "Cross-platform colored terminal text" }, + { "name": "colorcet", "uri": "https://anaconda.org/anaconda/colorcet", "description": "Collection of perceptually uniform colormaps" }, + { "name": "coloredlogs", "uri": "https://anaconda.org/anaconda/coloredlogs", "description": "Colored terminal output for Python's logging module" }, + { "name": "colorful", "uri": "https://anaconda.org/anaconda/colorful", "description": "Terminal string styling done right, in Python" }, + { "name": "colorlog", "uri": "https://anaconda.org/anaconda/colorlog", "description": "Log formatting with colors!" }, + { "name": "colorspacious", "uri": "https://anaconda.org/anaconda/colorspacious", "description": "A powerful, accurate, and easy-to-use Python library for doing colorspace conversions" }, + { "name": "colour", "uri": "https://anaconda.org/anaconda/colour", "description": "Python color representations manipulation library (RGB, HSL, web, ...)" }, + { "name": "comm", "uri": "https://anaconda.org/anaconda/comm", "description": "Python Comm implementation for the Jupyter kernel protocol" }, + { "name": "commonmark", "uri": "https://anaconda.org/anaconda/commonmark", "description": "Python parser for the CommonMark Markdown spec" }, + { "name": "compiler-rt", "uri": "https://anaconda.org/anaconda/compiler-rt", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_linux-64", "uri": "https://anaconda.org/anaconda/compiler-rt_linux-64", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_linux-aarch64", "uri": "https://anaconda.org/anaconda/compiler-rt_linux-aarch64", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_linux-ppc64le", "uri": "https://anaconda.org/anaconda/compiler-rt_linux-ppc64le", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_osx-64", "uri": "https://anaconda.org/anaconda/compiler-rt_osx-64", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_osx-arm64", "uri": "https://anaconda.org/anaconda/compiler-rt_osx-arm64", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_win-32", "uri": "https://anaconda.org/anaconda/compiler-rt_win-32", "description": "compiler-rt runtime libraries" }, + { "name": "compiler-rt_win-64", "uri": "https://anaconda.org/anaconda/compiler-rt_win-64", "description": "compiler-rt runtime libraries" }, + { "name": "composeml", "uri": "https://anaconda.org/anaconda/composeml", "description": "An open source python library for automated prediction engineering." }, + { "name": "comtypes", "uri": "https://anaconda.org/anaconda/comtypes", "description": "pure Python COM package" }, + { "name": "conda", "uri": "https://anaconda.org/anaconda/conda", "description": "OS-agnostic, system-level binary package and environment manager." }, + { "name": "conda-anaconda-telemetry", "uri": "https://anaconda.org/anaconda/conda-anaconda-telemetry", "description": "Anaconda Telemetry conda plugin" }, + { "name": "conda-anaconda-tos", "uri": "https://anaconda.org/anaconda/conda-anaconda-tos", "description": "Anaconda Terms of Service conda plugin" }, + { "name": "conda-audit", "uri": "https://anaconda.org/anaconda/conda-audit", "description": "Construct SBOM from Conda env with CVEs" }, + { "name": "conda-auth", "uri": "https://anaconda.org/anaconda/conda-auth", "description": "Conda plugin for improved access to private channels" }, + { "name": "conda-build", "uri": "https://anaconda.org/anaconda/conda-build", "description": "tools for building conda packages" }, + { "name": "conda-content-trust", "uri": "https://anaconda.org/anaconda/conda-content-trust", "description": "Signing and verification tools for conda" }, + { "name": "conda-index", "uri": "https://anaconda.org/anaconda/conda-index", "description": "Create `repodata.json` for collections of conda packages." }, + { "name": "conda-libmamba-solver", "uri": "https://anaconda.org/anaconda/conda-libmamba-solver", "description": "The fast mamba solver, now in conda!" }, + { "name": "conda-lock", "uri": "https://anaconda.org/anaconda/conda-lock", "description": "Lightweight lockfile for conda environments" }, + { "name": "conda-pack", "uri": "https://anaconda.org/anaconda/conda-pack", "description": "Package conda environments for redistribution" }, + { "name": "conda-package-handling", "uri": "https://anaconda.org/anaconda/conda-package-handling", "description": "Create and extract conda packages of various formats." }, + { "name": "conda-package-streaming", "uri": "https://anaconda.org/anaconda/conda-package-streaming", "description": "An efficient library to read from new and old format .conda and .tar.bz2 conda packages." }, + { "name": "conda-prefix-replacement", "uri": "https://anaconda.org/anaconda/conda-prefix-replacement", "description": "Detect and replace prefix paths embedded in files" }, + { "name": "conda-project", "uri": "https://anaconda.org/anaconda/conda-project", "description": "Tool for encapsulating, running, and reproducing projects with conda environments" }, + { "name": "conda-recipe-manager", "uri": "https://anaconda.org/anaconda/conda-recipe-manager", "description": "Helper tool for recipes on aggregate." }, + { "name": "conda-repo-cli", "uri": "https://anaconda.org/anaconda/conda-repo-cli", "description": "Anaconda Repository command line client library" }, + { "name": "conda-souschef", "uri": "https://anaconda.org/anaconda/conda-souschef", "description": "Project to handle conda recipes" }, + { "name": "conda-standalone", "uri": "https://anaconda.org/anaconda/conda-standalone", "description": "Entry point and dependency collection for PyInstaller-based standalone conda." }, + { "name": "conda-token", "uri": "https://anaconda.org/anaconda/conda-token", "description": "Set repository access token" }, + { "name": "confection", "uri": "https://anaconda.org/anaconda/confection", "description": "The sweetest config system for Python" }, + { "name": "configspace", "uri": "https://anaconda.org/anaconda/configspace", "description": "Creation and manipulation of parameter configuration spaces for automated algorithm configuration and hyperparameter tuning." }, + { "name": "configupdater", "uri": "https://anaconda.org/anaconda/configupdater", "description": "Parser like ConfigParser but for updating configuration files" }, + { "name": "configurable-http-proxy", "uri": "https://anaconda.org/anaconda/configurable-http-proxy", "description": "node-http-proxy plus a REST API" }, + { "name": "confuse", "uri": "https://anaconda.org/anaconda/confuse", "description": "painless YAML configuration" }, + { "name": "conllu", "uri": "https://anaconda.org/anaconda/conllu", "description": "CoNLL-U Parser parses a CoNLL-U formatted string into a nested python dictionary" }, + { "name": "connexion", "uri": "https://anaconda.org/anaconda/connexion", "description": "Swagger/OpenAPI First framework for Python on top of Flask with automatic endpoint validation & OAuth2 support" }, + { "name": "cons", "uri": "https://anaconda.org/anaconda/cons", "description": "An implementation of Lisp/Scheme-like cons in Python." }, + { "name": "console_shortcut", "uri": "https://anaconda.org/anaconda/console_shortcut", "description": "No Summary" }, + { "name": "console_shortcut_miniconda", "uri": "https://anaconda.org/anaconda/console_shortcut_miniconda", "description": "Console shortcut creator for Windows (using menuinst)" }, + { "name": "constantly", "uri": "https://anaconda.org/anaconda/constantly", "description": "Symbolic constants in Python" }, + { "name": "constructor", "uri": "https://anaconda.org/anaconda/constructor", "description": "create installer from conda packages" }, + { "name": "contextlib2", "uri": "https://anaconda.org/anaconda/contextlib2", "description": "Backports and enhancements for the contextlib module" }, + { "name": "contextvars", "uri": "https://anaconda.org/anaconda/contextvars", "description": "PEP 567 Backport" }, + { "name": "contourpy", "uri": "https://anaconda.org/anaconda/contourpy", "description": "Python library for calculating contours of 2D quadrilateral grids." }, + { "name": "convertdate", "uri": "https://anaconda.org/anaconda/convertdate", "description": "Converts between Gregorian dates and other calendar systems.Calendars included: Baha'i, French Republican, Hebrew, Indian Civil, Islamic, ISO, Julian, Mayan and Persian." }, + { "name": "cookiecutter", "uri": "https://anaconda.org/anaconda/cookiecutter", "description": "A command-line utility that creates projects from cookiecutters (project templates), e.g. creating a Python package project from a Python package project template." }, + { "name": "copy-jdk-configs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/copy-jdk-configs-cos6-x86_64", "description": "(CDT) JDKs configuration files copier" }, + { "name": "copy-jdk-configs-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/copy-jdk-configs-cos7-ppc64le", "description": "(CDT) JDKs configuration files copier" }, + { "name": "copy-jdk-configs-cos7-s390x", "uri": "https://anaconda.org/anaconda/copy-jdk-configs-cos7-s390x", "description": "(CDT) JDKs configuration files copier" }, + { "name": "coremltools", "uri": "https://anaconda.org/anaconda/coremltools", "description": "Core ML is an Apple framework to integrate machine learning models into your app. Core ML provides a unified representation for all models." }, + { "name": "coreutils", "uri": "https://anaconda.org/anaconda/coreutils", "description": "The GNU Core Utilities are the basic file, shell and text manipulation utilities of the GNU operating system." }, + { "name": "coreutils-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/coreutils-amzn2-aarch64", "description": "(CDT) A set of basic GNU tools commonly used in shell scripts" }, + { "name": "cornac", "uri": "https://anaconda.org/anaconda/cornac", "description": "A Comparative Framework for Multimodal Recommender Systems" }, + { "name": "cornice", "uri": "https://anaconda.org/anaconda/cornice", "description": "build and document Web Services with Pyramid" }, + { "name": "coverage", "uri": "https://anaconda.org/anaconda/coverage", "description": "Code coverage measurement for Python" }, + { "name": "coveralls", "uri": "https://anaconda.org/anaconda/coveralls", "description": "Show coverage stats online via coveralls.io" }, + { "name": "covid-sim", "uri": "https://anaconda.org/anaconda/covid-sim", "description": "COVID-19 CovidSim Model" }, + { "name": "covid-sim-data", "uri": "https://anaconda.org/anaconda/covid-sim-data", "description": "COVID-19 CovidSim Model" }, + { "name": "cpp-expected", "uri": "https://anaconda.org/anaconda/cpp-expected", "description": "C++11/14/17 std::expected with functional-style extensions" }, + { "name": "cpp-filesystem", "uri": "https://anaconda.org/anaconda/cpp-filesystem", "description": "An implementation of C++17 std::filesystem" }, + { "name": "cppy", "uri": "https://anaconda.org/anaconda/cppy", "description": "C++ headers for C extension development" }, + { "name": "cracklib-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cracklib-amzn2-aarch64", "description": "(CDT) A password-checking library" }, + { "name": "cracklib-cos6-x86_64", "uri": "https://anaconda.org/anaconda/cracklib-cos6-x86_64", "description": "(CDT) A password-checking library" }, + { "name": "cracklib-dicts-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cracklib-dicts-amzn2-aarch64", "description": "(CDT) The standard CrackLib dictionaries" }, + { "name": "cracklib-dicts-cos6-x86_64", "uri": "https://anaconda.org/anaconda/cracklib-dicts-cos6-x86_64", "description": "(CDT) The standard CrackLib dictionaries" }, + { "name": "cramjam", "uri": "https://anaconda.org/anaconda/cramjam", "description": "python bindings to rust-implemented compression" }, + { "name": "crashtest", "uri": "https://anaconda.org/anaconda/crashtest", "description": "Manage Python errors with ease" }, + { "name": "crender", "uri": "https://anaconda.org/anaconda/crender", "description": "The crender tool for creating and checking conda recipes" }, + { "name": "cron-descriptor", "uri": "https://anaconda.org/anaconda/cron-descriptor", "description": "A Python library that converts cron expressions into human readable strings." }, + { "name": "croniter", "uri": "https://anaconda.org/anaconda/croniter", "description": "croniter provides iteration for datetime object with cron like format" }, + { "name": "crosstool-ng", "uri": "https://anaconda.org/anaconda/crosstool-ng", "description": "A versatile (cross-)toolchain generator." }, + { "name": "cryptography", "uri": "https://anaconda.org/anaconda/cryptography", "description": "Provides cryptographic recipes and primitives to Python developers" }, + { "name": "cryptography-vectors", "uri": "https://anaconda.org/anaconda/cryptography-vectors", "description": "Test vectors for cryptography." }, + { "name": "cryptominisat", "uri": "https://anaconda.org/anaconda/cryptominisat", "description": "An advanced SAT Solver https://www.msoos.org" }, + { "name": "cryptsetup-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cryptsetup-libs-amzn2-aarch64", "description": "(CDT) Cryptsetup shared library" }, + { "name": "cssselect", "uri": "https://anaconda.org/anaconda/cssselect", "description": "CSS Selectors for Python" }, + { "name": "csvkit", "uri": "https://anaconda.org/anaconda/csvkit", "description": "A suite of command-line tools for working with CSV, the king of tabular file formats." }, + { "name": "ctypesgen", "uri": "https://anaconda.org/anaconda/ctypesgen", "description": "Python wrapper generator for ctypes" }, + { "name": "ctypesgen-pypdfium2-team", "uri": "https://anaconda.org/anaconda/ctypesgen-pypdfium2-team", "description": "Python wrapper generator for ctypes" }, + { "name": "cuda", "uri": "https://anaconda.org/anaconda/cuda", "description": "Meta-package containing all the available packages for native CUDA development" }, + { "name": "cuda-cccl", "uri": "https://anaconda.org/anaconda/cuda-cccl", "description": "CUDA C++ Core Libraries" }, + { "name": "cuda-cccl_linux-64", "uri": "https://anaconda.org/anaconda/cuda-cccl_linux-64", "description": "CUDA C++ Core Libraries" }, + { "name": "cuda-cccl_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-cccl_linux-aarch64", "description": "CUDA C++ Core Libraries" }, + { "name": "cuda-cccl_win-64", "uri": "https://anaconda.org/anaconda/cuda-cccl_win-64", "description": "CUDA C++ Core Libraries" }, + { "name": "cuda-command-line-tools", "uri": "https://anaconda.org/anaconda/cuda-command-line-tools", "description": "Meta-package containing the command line tools to debug CUDA applications" }, + { "name": "cuda-compat", "uri": "https://anaconda.org/anaconda/cuda-compat", "description": "CUDA Compatibility Platform" }, + { "name": "cuda-compat-impl", "uri": "https://anaconda.org/anaconda/cuda-compat-impl", "description": "CUDA Compatibility Platform" }, + { "name": "cuda-compiler", "uri": "https://anaconda.org/anaconda/cuda-compiler", "description": "A meta-package containing tools to start developing a CUDA application" }, + { "name": "cuda-crt", "uri": "https://anaconda.org/anaconda/cuda-crt", "description": "CUDA internal headers." }, + { "name": "cuda-crt-dev_linux-64", "uri": "https://anaconda.org/anaconda/cuda-crt-dev_linux-64", "description": "CUDA internal headers." }, + { "name": "cuda-crt-dev_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-crt-dev_linux-aarch64", "description": "CUDA internal headers." }, + { "name": "cuda-crt-dev_win-64", "uri": "https://anaconda.org/anaconda/cuda-crt-dev_win-64", "description": "CUDA internal headers." }, + { "name": "cuda-crt-tools", "uri": "https://anaconda.org/anaconda/cuda-crt-tools", "description": "CUDA internal tools." }, + { "name": "cuda-cudart", "uri": "https://anaconda.org/anaconda/cuda-cudart", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-dev", "uri": "https://anaconda.org/anaconda/cuda-cudart-dev", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-dev_linux-64", "uri": "https://anaconda.org/anaconda/cuda-cudart-dev_linux-64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-dev_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-cudart-dev_linux-aarch64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-dev_win-64", "uri": "https://anaconda.org/anaconda/cuda-cudart-dev_win-64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-static", "uri": "https://anaconda.org/anaconda/cuda-cudart-static", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-static_linux-64", "uri": "https://anaconda.org/anaconda/cuda-cudart-static_linux-64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-static_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-cudart-static_linux-aarch64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart-static_win-64", "uri": "https://anaconda.org/anaconda/cuda-cudart-static_win-64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-cudart_linux-64", "uri": "https://anaconda.org/anaconda/cuda-cudart_linux-64", "description": "CUDA Runtime architecture dependent libraries" }, + { "name": "cuda-cudart_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-cudart_linux-aarch64", "description": "CUDA Runtime architecture dependent libraries" }, + { "name": "cuda-cudart_win-64", "uri": "https://anaconda.org/anaconda/cuda-cudart_win-64", "description": "CUDA Runtime architecture dependent libraries" }, + { "name": "cuda-cuobjdump", "uri": "https://anaconda.org/anaconda/cuda-cuobjdump", "description": "Extracts information from CUDA binary files" }, + { "name": "cuda-cupti", "uri": "https://anaconda.org/anaconda/cuda-cupti", "description": "Provides libraries to enable third party tools using GPU profiling APIs." }, + { "name": "cuda-cupti-dev", "uri": "https://anaconda.org/anaconda/cuda-cupti-dev", "description": "Provides libraries to enable third party tools using GPU profiling APIs." }, + { "name": "cuda-cupti-doc", "uri": "https://anaconda.org/anaconda/cuda-cupti-doc", "description": "Provides libraries to enable third party tools using GPU profiling APIs." }, + { "name": "cuda-cupti-static", "uri": "https://anaconda.org/anaconda/cuda-cupti-static", "description": "Provides libraries to enable third party tools using GPU profiling APIs." }, + { "name": "cuda-cuxxfilt", "uri": "https://anaconda.org/anaconda/cuda-cuxxfilt", "description": "cu++filt decodes low-level identifiers that have been mangled by CUDA C++" }, + { "name": "cuda-driver-dev", "uri": "https://anaconda.org/anaconda/cuda-driver-dev", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-driver-dev_linux-64", "uri": "https://anaconda.org/anaconda/cuda-driver-dev_linux-64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-driver-dev_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-driver-dev_linux-aarch64", "description": "CUDA Runtime Native Libraries" }, + { "name": "cuda-gdb", "uri": "https://anaconda.org/anaconda/cuda-gdb", "description": "CUDA-GDB is the NVIDIA tool for debugging CUDA applications" }, + { "name": "cuda-gdb-src", "uri": "https://anaconda.org/anaconda/cuda-gdb-src", "description": "CUDA-GDB is the NVIDIA tool for debugging CUDA applications" }, + { "name": "cuda-libraries", "uri": "https://anaconda.org/anaconda/cuda-libraries", "description": "Meta-package containing all available library runtime packages." }, + { "name": "cuda-libraries-dev", "uri": "https://anaconda.org/anaconda/cuda-libraries-dev", "description": "Meta-package containing all available library development packages." }, + { "name": "cuda-libraries-static", "uri": "https://anaconda.org/anaconda/cuda-libraries-static", "description": "Meta-package containing all available library static packages." }, + { "name": "cuda-minimal-build", "uri": "https://anaconda.org/anaconda/cuda-minimal-build", "description": "Meta-package containing the minimal necessary to build basic CUDA apps." }, + { "name": "cuda-nsight", "uri": "https://anaconda.org/anaconda/cuda-nsight", "description": "A unified CPU plus GPU IDE for developing CUDA applications" }, + { "name": "cuda-nvcc", "uri": "https://anaconda.org/anaconda/cuda-nvcc", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvcc-dev_linux-64", "uri": "https://anaconda.org/anaconda/cuda-nvcc-dev_linux-64", "description": "Target architecture dependent parts of CUDA NVCC compiler." }, + { "name": "cuda-nvcc-dev_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-nvcc-dev_linux-aarch64", "description": "Target architecture dependent parts of CUDA NVCC compiler." }, + { "name": "cuda-nvcc-dev_win-64", "uri": "https://anaconda.org/anaconda/cuda-nvcc-dev_win-64", "description": "Target architecture dependent parts of CUDA NVCC compiler." }, + { "name": "cuda-nvcc-impl", "uri": "https://anaconda.org/anaconda/cuda-nvcc-impl", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvcc-tools", "uri": "https://anaconda.org/anaconda/cuda-nvcc-tools", "description": "Architecture independent part of CUDA NVCC compiler." }, + { "name": "cuda-nvcc_linux-64", "uri": "https://anaconda.org/anaconda/cuda-nvcc_linux-64", "description": "Compiler activation scripts for CUDA applications." }, + { "name": "cuda-nvcc_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-nvcc_linux-aarch64", "description": "Compiler activation scripts for CUDA applications." }, + { "name": "cuda-nvcc_win-64", "uri": "https://anaconda.org/anaconda/cuda-nvcc_win-64", "description": "Compiler activation scripts for CUDA applications." }, + { "name": "cuda-nvdisasm", "uri": "https://anaconda.org/anaconda/cuda-nvdisasm", "description": "nvdisasm extracts information from standalone cubin files" }, + { "name": "cuda-nvml-dev", "uri": "https://anaconda.org/anaconda/cuda-nvml-dev", "description": "NVML native dev links, headers" }, + { "name": "cuda-nvprof", "uri": "https://anaconda.org/anaconda/cuda-nvprof", "description": "Tool for collecting and viewing CUDA application profiling data" }, + { "name": "cuda-nvprune", "uri": "https://anaconda.org/anaconda/cuda-nvprune", "description": "Prunes host object files and libraries to only contain device code" }, + { "name": "cuda-nvrtc", "uri": "https://anaconda.org/anaconda/cuda-nvrtc", "description": "NVRTC native runtime libraries" }, + { "name": "cuda-nvrtc-dev", "uri": "https://anaconda.org/anaconda/cuda-nvrtc-dev", "description": "NVRTC native runtime libraries" }, + { "name": "cuda-nvrtc-static", "uri": "https://anaconda.org/anaconda/cuda-nvrtc-static", "description": "NVRTC native runtime libraries" }, + { "name": "cuda-nvtx", "uri": "https://anaconda.org/anaconda/cuda-nvtx", "description": "A C-based API for annotating events, code ranges, and resources" }, + { "name": "cuda-nvtx-dev", "uri": "https://anaconda.org/anaconda/cuda-nvtx-dev", "description": "A C-based API for annotating events, code ranges, and resources" }, + { "name": "cuda-nvvm", "uri": "https://anaconda.org/anaconda/cuda-nvvm", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvm-dev_linux-64", "uri": "https://anaconda.org/anaconda/cuda-nvvm-dev_linux-64", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvm-dev_linux-aarch64", "uri": "https://anaconda.org/anaconda/cuda-nvvm-dev_linux-aarch64", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvm-dev_win-64", "uri": "https://anaconda.org/anaconda/cuda-nvvm-dev_win-64", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvm-impl", "uri": "https://anaconda.org/anaconda/cuda-nvvm-impl", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvm-tools", "uri": "https://anaconda.org/anaconda/cuda-nvvm-tools", "description": "Compiler for CUDA applications." }, + { "name": "cuda-nvvp", "uri": "https://anaconda.org/anaconda/cuda-nvvp", "description": "NVIDIA Visual Profiler to visualize and optimize the performance of your application." }, + { "name": "cuda-opencl", "uri": "https://anaconda.org/anaconda/cuda-opencl", "description": "CUDA OpenCL native Libraries" }, + { "name": "cuda-opencl-dev", "uri": "https://anaconda.org/anaconda/cuda-opencl-dev", "description": "CUDA OpenCL native Libraries" }, + { "name": "cuda-profiler-api", "uri": "https://anaconda.org/anaconda/cuda-profiler-api", "description": "CUDA Profiler API Headers." }, + { "name": "cuda-runtime", "uri": "https://anaconda.org/anaconda/cuda-runtime", "description": "Meta-package containing all runtime library packages." }, + { "name": "cuda-sanitizer-api", "uri": "https://anaconda.org/anaconda/cuda-sanitizer-api", "description": "Provides a set of APIs to enable third party tools to write GPU sanitizing tools" }, + { "name": "cuda-toolkit", "uri": "https://anaconda.org/anaconda/cuda-toolkit", "description": "Meta-package containing all toolkit packages for CUDA development" }, + { "name": "cuda-tools", "uri": "https://anaconda.org/anaconda/cuda-tools", "description": "Meta-package containing all CUDA command line and visual tools." }, + { "name": "cuda-version", "uri": "https://anaconda.org/anaconda/cuda-version", "description": "A meta-package for pinning to a CUDA release version" }, + { "name": "cuda-visual-tools", "uri": "https://anaconda.org/anaconda/cuda-visual-tools", "description": "Contains the visual tools to debug and profile CUDA applications" }, + { "name": "cudnn", "uri": "https://anaconda.org/anaconda/cudnn", "description": "NVIDIA's cuDNN deep neural network acceleration library" }, + { "name": "cunit", "uri": "https://anaconda.org/anaconda/cunit", "description": "A Unit Testing Framework for C" }, + { "name": "cups-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cups-devel-amzn2-aarch64", "description": "(CDT) CUPS printing system - development environment" }, + { "name": "cups-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/cups-devel-cos6-i686", "description": "(CDT) Common Unix Printing System - development environment" }, + { "name": "cups-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/cups-devel-cos6-x86_64", "description": "(CDT) Common Unix Printing System - development environment" }, + { "name": "cups-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/cups-devel-cos7-s390x", "description": "(CDT) CUPS printing system - development environment" }, + { "name": "cups-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/cups-libs-amzn2-aarch64", "description": "(CDT) CUPS printing system - libraries" }, + { "name": "cups-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/cups-libs-cos6-i686", "description": "(CDT) Common Unix Printing System - libraries" }, + { "name": "cups-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/cups-libs-cos6-x86_64", "description": "(CDT) Common Unix Printing System - libraries" }, + { "name": "cupti", "uri": "https://anaconda.org/anaconda/cupti", "description": "development environment for GPU-accelerated applications, CUPTI components" }, + { "name": "cupy", "uri": "https://anaconda.org/anaconda/cupy", "description": "CuPy is an implementation of a NumPy-compatible multi-dimensional array on CUDA." }, + { "name": "curio", "uri": "https://anaconda.org/anaconda/curio", "description": "The coroutine concurrency library." }, + { "name": "curl", "uri": "https://anaconda.org/anaconda/curl", "description": "tool and library for transferring data with URL syntax" }, + { "name": "curtsies", "uri": "https://anaconda.org/anaconda/curtsies", "description": "Curses-like terminal wrapper, with colored strings!" }, + { "name": "cutensor", "uri": "https://anaconda.org/anaconda/cutensor", "description": "Tensor Linear Algebra on NVIDIA GPUs" }, + { "name": "cvxcanon", "uri": "https://anaconda.org/anaconda/cvxcanon", "description": "Low-level library to perform the matrix building step in CVXPY" }, + { "name": "cvxopt", "uri": "https://anaconda.org/anaconda/cvxopt", "description": "Convex optimization package" }, + { "name": "cx_oracle", "uri": "https://anaconda.org/anaconda/cx_oracle", "description": "Python interface to Oracle" }, + { "name": "cymem", "uri": "https://anaconda.org/anaconda/cymem", "description": "Manage calls to calloc/free through Cython" }, + { "name": "cyrus-sasl", "uri": "https://anaconda.org/anaconda/cyrus-sasl", "description": "This is the Cyrus SASL API implementation. It can be used on the client or server side to provide\nauthentication and authorization services. See RFC 4422 for more information." }, + { "name": "cython", "uri": "https://anaconda.org/anaconda/cython", "description": "The Cython compiler for writing C extensions for the Python language" }, + { "name": "cython-blis", "uri": "https://anaconda.org/anaconda/cython-blis", "description": "Fast matrix-multiplication as a self-contained Python library - no system dependencies!" }, + { "name": "cytoolz", "uri": "https://anaconda.org/anaconda/cytoolz", "description": "Cython implementation of Toolz. High performance functional utilities" }, + { "name": "d2to1", "uri": "https://anaconda.org/anaconda/d2to1", "description": "Allows using distutils2-like setup.cfg files for a package's metadata with a distribute/setuptools setup.py" }, + { "name": "daal", "uri": "https://anaconda.org/anaconda/daal", "description": "DAAL runtime libraries" }, + { "name": "daal-devel", "uri": "https://anaconda.org/anaconda/daal-devel", "description": "Devel package for building things linked against DAAL shared libraries" }, + { "name": "daal-include", "uri": "https://anaconda.org/anaconda/daal-include", "description": "Headers for building against DAAL libraries" }, + { "name": "daal-static", "uri": "https://anaconda.org/anaconda/daal-static", "description": "Static libraries for DAAL" }, + { "name": "daal4py", "uri": "https://anaconda.org/anaconda/daal4py", "description": "A convenient Python API to Intel (R) oneAPI Data Analytics Library" }, + { "name": "dacite", "uri": "https://anaconda.org/anaconda/dacite", "description": "Simple creation of data classes from dictionaries." }, + { "name": "dal", "uri": "https://anaconda.org/anaconda/dal", "description": "Intel® oneDAL runtime libraries" }, + { "name": "dal-devel", "uri": "https://anaconda.org/anaconda/dal-devel", "description": "Devel package for building things linked against Intel® oneDAL shared libraries" }, + { "name": "dal-include", "uri": "https://anaconda.org/anaconda/dal-include", "description": "Headers for building against Intel® oneDAL libraries" }, + { "name": "dal-static", "uri": "https://anaconda.org/anaconda/dal-static", "description": "Static libraries for Intel® oneDAL" }, + { "name": "dash", "uri": "https://anaconda.org/anaconda/dash", "description": "A Python framework for building reactive web-apps." }, + { "name": "dash-bio", "uri": "https://anaconda.org/anaconda/dash-bio", "description": "dash_bio" }, + { "name": "dash-core-components", "uri": "https://anaconda.org/anaconda/dash-core-components", "description": "Dash UI core component suite" }, + { "name": "dash-html-components", "uri": "https://anaconda.org/anaconda/dash-html-components", "description": "Dash UI HTML component suite" }, + { "name": "dash-renderer", "uri": "https://anaconda.org/anaconda/dash-renderer", "description": "Front-end component renderer for dash" }, + { "name": "dash-table", "uri": "https://anaconda.org/anaconda/dash-table", "description": "A First-Class Interactive DataTable for Dash" }, + { "name": "dash_cytoscape", "uri": "https://anaconda.org/anaconda/dash_cytoscape", "description": "A Component Library for Dash aimed at facilitating network visualization in Python, wrapped around Cytoscape.js" }, + { "name": "dask", "uri": "https://anaconda.org/anaconda/dask", "description": "Parallel PyData with Task Scheduling" }, + { "name": "dask-core", "uri": "https://anaconda.org/anaconda/dask-core", "description": "Parallel Python with task scheduling" }, + { "name": "dask-expr", "uri": "https://anaconda.org/anaconda/dask-expr", "description": "High Level Expressions for Dask" }, + { "name": "dask-glm", "uri": "https://anaconda.org/anaconda/dask-glm", "description": "Generalized Linear Models in Dask" }, + { "name": "dask-image", "uri": "https://anaconda.org/anaconda/dask-image", "description": "Distributed image processing" }, + { "name": "dask-jobqueue", "uri": "https://anaconda.org/anaconda/dask-jobqueue", "description": "Easy deployment of Dask Distributed on job queuing systems like PBS, Slurm, LSF and SGE." }, + { "name": "dask-ml", "uri": "https://anaconda.org/anaconda/dask-ml", "description": "Distributed and parallel machine learning using dask." }, + { "name": "dask-searchcv", "uri": "https://anaconda.org/anaconda/dask-searchcv", "description": "Tools for doing hyperparameter search with Scikit-Learn and Dask" }, + { "name": "databricks-cli", "uri": "https://anaconda.org/anaconda/databricks-cli", "description": "A command line interface for Databricks" }, + { "name": "databricks-sdk", "uri": "https://anaconda.org/anaconda/databricks-sdk", "description": "Databricks SDK for Python (Experimental)" }, + { "name": "dataclasses", "uri": "https://anaconda.org/anaconda/dataclasses", "description": "An implementation of PEP 557: Data Classes" }, + { "name": "dataclasses-json", "uri": "https://anaconda.org/anaconda/dataclasses-json", "description": "Easily serialize dataclasses to and from JSON" }, + { "name": "datadog", "uri": "https://anaconda.org/anaconda/datadog", "description": "The Datadog Python library" }, + { "name": "datasets", "uri": "https://anaconda.org/anaconda/datasets", "description": "HuggingFace community-driven open-source library of datasets" }, + { "name": "datashader", "uri": "https://anaconda.org/anaconda/datashader", "description": "Data visualization toolchain based on aggregating into a grid" }, + { "name": "datashape", "uri": "https://anaconda.org/anaconda/datashape", "description": "No Summary" }, + { "name": "dateparser", "uri": "https://anaconda.org/anaconda/dateparser", "description": "Date parsing library designed to parse dates from HTML pages" }, + { "name": "dateutils", "uri": "https://anaconda.org/anaconda/dateutils", "description": "Various utilities for working with date and datetime objects" }, + { "name": "datrie", "uri": "https://anaconda.org/anaconda/datrie", "description": "Super-fast, efficiently stored Trie for Python, uses libdatrie" }, + { "name": "dav1d", "uri": "https://anaconda.org/anaconda/dav1d", "description": "dav1d is the fastest AV1 decoder on all platforms" }, + { "name": "dawg-python", "uri": "https://anaconda.org/anaconda/dawg-python", "description": "Pure-python reader for DAWGs (DAFSAs) created by dawgdic C++ library or DAWG Python extension." }, + { "name": "db4-cos6-x86_64", "uri": "https://anaconda.org/anaconda/db4-cos6-x86_64", "description": "(CDT) The Berkeley DB database library (version 4) for C" }, + { "name": "db4-cxx-cos6-x86_64", "uri": "https://anaconda.org/anaconda/db4-cxx-cos6-x86_64", "description": "(CDT) The Berkeley DB database library (version 4) for C++" }, + { "name": "db4-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/db4-devel-cos6-x86_64", "description": "(CDT) C development files for the Berkeley DB (version 4) library" }, + { "name": "dbfread", "uri": "https://anaconda.org/anaconda/dbfread", "description": "read data from dbf files" }, + { "name": "dbt-extractor", "uri": "https://anaconda.org/anaconda/dbt-extractor", "description": "Jinja value templates for dbt model files" }, + { "name": "dbus", "uri": "https://anaconda.org/anaconda/dbus", "description": "Simple message bus system for applications to talk to one another" }, + { "name": "dbus-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/dbus-amzn2-aarch64", "description": "(CDT) D-BUS message bus" }, + { "name": "dbus-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/dbus-libs-amzn2-aarch64", "description": "(CDT) Libraries for accessing D-BUS" }, + { "name": "dbus-python", "uri": "https://anaconda.org/anaconda/dbus-python", "description": "Python bindings for dbus" }, + { "name": "debugpy", "uri": "https://anaconda.org/anaconda/debugpy", "description": "An implementation of the Debug Adapter Protocol for Python" }, + { "name": "deepdiff", "uri": "https://anaconda.org/anaconda/deepdiff", "description": "Deep Difference and Search of any Python object/data." }, + { "name": "defusedxml", "uri": "https://anaconda.org/anaconda/defusedxml", "description": "XML bomb protection for Python stdlib modules" }, + { "name": "dejavu-fonts-common-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/dejavu-fonts-common-amzn2-aarch64", "description": "(CDT) Common files for the Dejavu font set" }, + { "name": "dejavu-sans-fonts-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/dejavu-sans-fonts-amzn2-aarch64", "description": "(CDT) Variable-width sans-serif font faces" }, + { "name": "deprecated", "uri": "https://anaconda.org/anaconda/deprecated", "description": "Python @deprecated decorator to deprecate old python classes, functions or methods." }, + { "name": "deprecation", "uri": "https://anaconda.org/anaconda/deprecation", "description": "A library to handle automated deprecations" }, + { "name": "descartes", "uri": "https://anaconda.org/anaconda/descartes", "description": "Use geometric objects as matplotlib paths and patches." }, + { "name": "device-mapper-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/device-mapper-libs-amzn2-aarch64", "description": "(CDT) Device-mapper shared library" }, + { "name": "devil", "uri": "https://anaconda.org/anaconda/devil", "description": "A full featured cross-platform image library." }, + { "name": "dictdiffer", "uri": "https://anaconda.org/anaconda/dictdiffer", "description": "Dictdiffer is a helper module that helps you to diff and patch dictionaries." }, + { "name": "dicttoxml", "uri": "https://anaconda.org/anaconda/dicttoxml", "description": "Converts a Python dictionary or other native data type into a valid XML string." }, + { "name": "diff-match-patch", "uri": "https://anaconda.org/anaconda/diff-match-patch", "description": "Diff Match Patch is a high-performance library in multiple languages that manipulates plain text" }, + { "name": "diffusers", "uri": "https://anaconda.org/anaconda/diffusers", "description": "Diffusers provides pretrained vision diffusion models, and serves as a modular toolbox for inference and training." }, + { "name": "diffusers-base", "uri": "https://anaconda.org/anaconda/diffusers-base", "description": "Diffusers provides pretrained vision diffusion models, and serves as a modular toolbox for inference and training." }, + { "name": "diffusers-torch", "uri": "https://anaconda.org/anaconda/diffusers-torch", "description": "Diffusers provides pretrained vision diffusion models, and serves as a modular toolbox for inference and training." }, + { "name": "diffutils-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/diffutils-amzn2-aarch64", "description": "(CDT) A GNU collection of diff utilities" }, + { "name": "dill", "uri": "https://anaconda.org/anaconda/dill", "description": "Serialize all of python (almost)" }, + { "name": "dis3", "uri": "https://anaconda.org/anaconda/dis3", "description": "Python 2.7 backport of the \"dis\" module from Python 3.5+" }, + { "name": "distconfig3", "uri": "https://anaconda.org/anaconda/distconfig3", "description": "Library to manage configuration using Zookeeper, Etcd, Consul" }, + { "name": "distlib", "uri": "https://anaconda.org/anaconda/distlib", "description": "Distribution utilities for python" }, + { "name": "distributed", "uri": "https://anaconda.org/anaconda/distributed", "description": "A distributed task scheduler for Dask" }, + { "name": "distro", "uri": "https://anaconda.org/anaconda/distro", "description": "A much more elaborate replacement for removed Python's 'platform.linux_distribution()' method" }, + { "name": "django", "uri": "https://anaconda.org/anaconda/django", "description": "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." }, + { "name": "django-pyodbc-azure", "uri": "https://anaconda.org/anaconda/django-pyodbc-azure", "description": "Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc" }, + { "name": "dlpack", "uri": "https://anaconda.org/anaconda/dlpack", "description": "DLPack - Open In Memory Tensor Structure" }, + { "name": "dm-tree", "uri": "https://anaconda.org/anaconda/dm-tree", "description": "Tree is a library for working with nested data structures." }, + { "name": "dmglib", "uri": "https://anaconda.org/anaconda/dmglib", "description": "Python library to work with macOS DMG disk images" }, + { "name": "dnspython", "uri": "https://anaconda.org/anaconda/dnspython", "description": "DNS toolkit" }, + { "name": "docker-py", "uri": "https://anaconda.org/anaconda/docker-py", "description": "Python client for Docker." }, + { "name": "docker-pycreds", "uri": "https://anaconda.org/anaconda/docker-pycreds", "description": "Python bindings for the docker credentials store API" }, + { "name": "docopt", "uri": "https://anaconda.org/anaconda/docopt", "description": "No Summary" }, + { "name": "docstring-to-markdown", "uri": "https://anaconda.org/anaconda/docstring-to-markdown", "description": "On the fly conversion of Python docstrings to markdown" }, + { "name": "docutils", "uri": "https://anaconda.org/anaconda/docutils", "description": "Docutils -- Python Documentation Utilities" }, + { "name": "doit", "uri": "https://anaconda.org/anaconda/doit", "description": "doit - Automation Tool" }, + { "name": "dos2unix", "uri": "https://anaconda.org/anaconda/dos2unix", "description": "Convert text files with DOS or Mac line breaks to Unix line breaks and vice versa." }, + { "name": "double-conversion", "uri": "https://anaconda.org/anaconda/double-conversion", "description": "Efficient binary-decimal and decimal-binary conversion routines for IEEE doubles." }, + { "name": "dpcpp-cpp-rt", "uri": "https://anaconda.org/anaconda/dpcpp-cpp-rt", "description": "Runtime for Intel® oneAPI DPC++/C++ Compiler" }, + { "name": "dpcpp_impl_linux-64", "uri": "https://anaconda.org/anaconda/dpcpp_impl_linux-64", "description": "Implementation for Intel® oneAPI DPC++/C++ Compiler" }, + { "name": "dpcpp_impl_win-64", "uri": "https://anaconda.org/anaconda/dpcpp_impl_win-64", "description": "Implementation for Intel® oneAPI DPC++/C++ Compiler" }, + { "name": "dpcpp_linux-64", "uri": "https://anaconda.org/anaconda/dpcpp_linux-64", "description": "Activation for Intel® oneAPI DPC++/C++ Compiler" }, + { "name": "dpcpp_win-64", "uri": "https://anaconda.org/anaconda/dpcpp_win-64", "description": "Activation for Intel® oneAPI DPC++/C++ Compiler" }, + { "name": "dpctl", "uri": "https://anaconda.org/anaconda/dpctl", "description": "A lightweight Python wrapper for a subset of SYCL API." }, + { "name": "dpl-include", "uri": "https://anaconda.org/anaconda/dpl-include", "description": "Intel® DPC++ Compilers (dpl component)" }, + { "name": "dpnp", "uri": "https://anaconda.org/anaconda/dpnp", "description": "NumPy Drop-In Replacement for Intel(R) XPU" }, + { "name": "draco", "uri": "https://anaconda.org/anaconda/draco", "description": "A library for compressing and decompressing 3D geometric meshes and point clouds" }, + { "name": "drmaa", "uri": "https://anaconda.org/anaconda/drmaa", "description": "Python wrapper around the C DRMAA library" }, + { "name": "dropbox", "uri": "https://anaconda.org/anaconda/dropbox", "description": "Official Dropbox API Client" }, + { "name": "dsdp", "uri": "https://anaconda.org/anaconda/dsdp", "description": "Software for semidefinite programming" }, + { "name": "dulwich", "uri": "https://anaconda.org/anaconda/dulwich", "description": "Python Git Library" }, + { "name": "duma_linux-32", "uri": "https://anaconda.org/anaconda/duma_linux-32", "description": "DUMA is an open-source library to detect buffer overruns and under-runs in C and C++ programs." }, + { "name": "duma_linux-64", "uri": "https://anaconda.org/anaconda/duma_linux-64", "description": "DUMA is an open-source library to detect buffer overruns and under-runs in C and C++ programs." }, + { "name": "duma_linux-aarch64", "uri": "https://anaconda.org/anaconda/duma_linux-aarch64", "description": "DUMA is an open-source library to detect buffer overruns and under-runs in C and C++ programs." }, + { "name": "duma_linux-ppc64le", "uri": "https://anaconda.org/anaconda/duma_linux-ppc64le", "description": "DUMA is an open-source library to detect buffer overruns and under-runs in C and C++ programs." }, + { "name": "duma_linux-s390x", "uri": "https://anaconda.org/anaconda/duma_linux-s390x", "description": "DUMA is an open-source library to detect buffer overruns and under-runs in C and C++ programs." }, + { "name": "dunamai", "uri": "https://anaconda.org/anaconda/dunamai", "description": "Dynamic versioning library and CLI" }, + { "name": "easyocr", "uri": "https://anaconda.org/anaconda/easyocr", "description": "End-to-End Multi-Lingual Optical Character Recognition (OCR) Solution" }, + { "name": "ecdsa", "uri": "https://anaconda.org/anaconda/ecdsa", "description": "ECDSA cryptographic signature library (pure python)" }, + { "name": "echo", "uri": "https://anaconda.org/anaconda/echo", "description": "Callback Properties in Python" }, + { "name": "ecos", "uri": "https://anaconda.org/anaconda/ecos", "description": "Python interface for ECOS a lightweight conic solver for second-order cone programming" }, + { "name": "editables", "uri": "https://anaconda.org/anaconda/editables", "description": "A Python library for creating \"editable wheels\"" }, + { "name": "editdistance", "uri": "https://anaconda.org/anaconda/editdistance", "description": "Fast implementation of the edit distance(Levenshtein distance)" }, + { "name": "efficientnet", "uri": "https://anaconda.org/anaconda/efficientnet", "description": "EfficientNet model re-implementation. Keras and TensorFlow Keras." }, + { "name": "eigen", "uri": "https://anaconda.org/anaconda/eigen", "description": "Eigen is a C++ template library for linear algebra: matrices, vectors, numerical solvers, and related algorithms." }, + { "name": "elasticsearch", "uri": "https://anaconda.org/anaconda/elasticsearch", "description": "Python client for Elasticsearch" }, + { "name": "elasticsearch-async", "uri": "https://anaconda.org/anaconda/elasticsearch-async", "description": "Async backend for elasticsearch-py" }, + { "name": "elasticsearch-dsl", "uri": "https://anaconda.org/anaconda/elasticsearch-dsl", "description": "Higher-level Python client for Elasticsearch" }, + { "name": "elementpath", "uri": "https://anaconda.org/anaconda/elementpath", "description": "XPath 1.0/2.0 parsers and selectors for ElementTree" }, + { "name": "elfutils-default-yama-scope-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/elfutils-default-yama-scope-amzn2-aarch64", "description": "(CDT) Default yama attach scope sysctl setting" }, + { "name": "elfutils-libelf-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/elfutils-libelf-amzn2-aarch64", "description": "(CDT) Library to read and write ELF files" }, + { "name": "elfutils-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/elfutils-libs-amzn2-aarch64", "description": "(CDT) Libraries to handle compiled objects" }, + { "name": "email-validator", "uri": "https://anaconda.org/anaconda/email-validator", "description": "A robust email syntax and deliverability validation library for 3.x." }, + { "name": "email_validator", "uri": "https://anaconda.org/anaconda/email_validator", "description": "A robust email syntax and deliverability validation library for 3.x." }, + { "name": "emfile", "uri": "https://anaconda.org/anaconda/emfile", "description": "Basic utility to read tomography data from files in `*.em` format." }, + { "name": "enaml", "uri": "https://anaconda.org/anaconda/enaml", "description": "Declarative DSL for building rich user interfaces in Python" }, + { "name": "enscons", "uri": "https://anaconda.org/anaconda/enscons", "description": "Tools for building Python packages with SCons. Experimental." }, + { "name": "ensureconda", "uri": "https://anaconda.org/anaconda/ensureconda", "description": "Install and run applications packaged with conda in isolated environments" }, + { "name": "entrypoints", "uri": "https://anaconda.org/anaconda/entrypoints", "description": "Discover and load entry points from installed packages." }, + { "name": "enum34", "uri": "https://anaconda.org/anaconda/enum34", "description": "Python 3.4 Enum backported to 3.3, 3.2, 3.1, 2.7, 2.6, 2.5, and 2.4" }, + { "name": "ephem", "uri": "https://anaconda.org/anaconda/ephem", "description": "Basic astronomical computations for Python" }, + { "name": "epoxy", "uri": "https://anaconda.org/anaconda/epoxy", "description": "A library for handling OpenGL function pointer management for you." }, + { "name": "esda", "uri": "https://anaconda.org/anaconda/esda", "description": "Exploratory Spatial Data Analysis" }, + { "name": "essential_generators", "uri": "https://anaconda.org/anaconda/essential_generators", "description": "Generate fake data for application testing based on simple but flexible templates." }, + { "name": "et_xmlfile", "uri": "https://anaconda.org/anaconda/et_xmlfile", "description": "An implementation of lxml.xmlfile for the standard library" }, + { "name": "etuples", "uri": "https://anaconda.org/anaconda/etuples", "description": "Python S-expression emulation using tuple-like objects." }, + { "name": "evaluate", "uri": "https://anaconda.org/anaconda/evaluate", "description": "HuggingFace community-driven open-source library of evaluation" }, + { "name": "eventlet", "uri": "https://anaconda.org/anaconda/eventlet", "description": "Highly concurrent networking library" }, + { "name": "exceptiongroup", "uri": "https://anaconda.org/anaconda/exceptiongroup", "description": "Backport of PEP 654 (exception groups)" }, + { "name": "execnet", "uri": "https://anaconda.org/anaconda/execnet", "description": "distributed Python deployment and communication" }, + { "name": "executing", "uri": "https://anaconda.org/anaconda/executing", "description": "Get the currently executing AST node of a frame, and other information" }, + { "name": "expandvars", "uri": "https://anaconda.org/anaconda/expandvars", "description": "Expand system variables Unix style" }, + { "name": "expat", "uri": "https://anaconda.org/anaconda/expat", "description": "Expat XML parser library in C" }, + { "name": "expat-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/expat-amzn2-aarch64", "description": "(CDT) An XML parser library" }, + { "name": "expat-cos6-i686", "uri": "https://anaconda.org/anaconda/expat-cos6-i686", "description": "(CDT) An XML parser library" }, + { "name": "expat-cos6-x86_64", "uri": "https://anaconda.org/anaconda/expat-cos6-x86_64", "description": "(CDT) An XML parser library" }, + { "name": "expat-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/expat-cos7-ppc64le", "description": "(CDT) An XML parser library" }, + { "name": "expat-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/expat-devel-cos6-i686", "description": "(CDT) Libraries and header files to develop applications using expat" }, + { "name": "expat-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/expat-devel-cos6-x86_64", "description": "(CDT) Libraries and header files to develop applications using expat" }, + { "name": "expat-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/expat-devel-cos7-ppc64le", "description": "(CDT) Libraries and header files to develop applications using expat" }, + { "name": "expecttest", "uri": "https://anaconda.org/anaconda/expecttest", "description": "This library implements expect tests (also known as \"golden\" tests)." }, + { "name": "extension-helpers", "uri": "https://anaconda.org/anaconda/extension-helpers", "description": "Utilities for building and installing packages with compiled extensions" }, + { "name": "factory_boy", "uri": "https://anaconda.org/anaconda/factory_boy", "description": "A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby." }, + { "name": "faker", "uri": "https://anaconda.org/anaconda/faker", "description": "Faker is a Python package that generates fake data for you" }, + { "name": "farama-notifications", "uri": "https://anaconda.org/anaconda/farama-notifications", "description": "Notifications for all Farama Foundation maintained libraries." }, + { "name": "fast-histogram", "uri": "https://anaconda.org/anaconda/fast-histogram", "description": "Fast 1D and 2D histogram functions in Python" }, + { "name": "fastapi", "uri": "https://anaconda.org/anaconda/fastapi", "description": "FastAPI framework, high performance, easy to learn, fast to code, ready for production" }, + { "name": "fastavro", "uri": "https://anaconda.org/anaconda/fastavro", "description": "Fast read/write of AVRO files" }, + { "name": "fastcache", "uri": "https://anaconda.org/anaconda/fastcache", "description": "C implementation of Python 3 lru_cache" }, + { "name": "fastcluster", "uri": "https://anaconda.org/anaconda/fastcluster", "description": "Fast hierarchical clustering routines for R and Python" }, + { "name": "fastcore", "uri": "https://anaconda.org/anaconda/fastcore", "description": "Python supercharged for fastai development" }, + { "name": "fastdownload", "uri": "https://anaconda.org/anaconda/fastdownload", "description": "A general purpose data downloading library." }, + { "name": "fasteners", "uri": "https://anaconda.org/anaconda/fasteners", "description": "A python package that provides useful locks." }, + { "name": "fastparquet", "uri": "https://anaconda.org/anaconda/fastparquet", "description": "Python interface to the parquet format" }, + { "name": "fastprogress", "uri": "https://anaconda.org/anaconda/fastprogress", "description": "A fast and simple progress bar for Jupyter Notebook and console." }, + { "name": "fastrlock", "uri": "https://anaconda.org/anaconda/fastrlock", "description": "This is a C-level implementation of a fast, re-entrant, optimistic lock for CPython" }, + { "name": "fasttsne", "uri": "https://anaconda.org/anaconda/fasttsne", "description": "Fast, parallel implementations of tSNE" }, + { "name": "favicon", "uri": "https://anaconda.org/anaconda/favicon", "description": "Get a website's favicon." }, + { "name": "featuretools", "uri": "https://anaconda.org/anaconda/featuretools", "description": "a framework for automated feature engineering" }, + { "name": "feedparser", "uri": "https://anaconda.org/anaconda/feedparser", "description": "parse feeds in Python" }, + { "name": "ffmpeg", "uri": "https://anaconda.org/anaconda/ffmpeg", "description": "Cross-platform solution to record, convert and stream audio and video." }, + { "name": "fftw", "uri": "https://anaconda.org/anaconda/fftw", "description": "The fastest Fourier transform in the west." }, + { "name": "file", "uri": "https://anaconda.org/anaconda/file", "description": "Fine Free File Command" }, + { "name": "filelock", "uri": "https://anaconda.org/anaconda/filelock", "description": "A platform independent file lock." }, + { "name": "filesystem-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/filesystem-amzn2-aarch64", "description": "(CDT) The basic directory layout for a Linux system" }, + { "name": "findpython", "uri": "https://anaconda.org/anaconda/findpython", "description": "A utility to find python versions on your system" }, + { "name": "fiona", "uri": "https://anaconda.org/anaconda/fiona", "description": "Fiona reads and writes spatial data files" }, + { "name": "flake8", "uri": "https://anaconda.org/anaconda/flake8", "description": "Your Tool For Style Guide Enforcement" }, + { "name": "flake8-import-order", "uri": "https://anaconda.org/anaconda/flake8-import-order", "description": "A flake8 and Pylama plugin that checks the ordering of your imports." }, + { "name": "flake8-polyfill", "uri": "https://anaconda.org/anaconda/flake8-polyfill", "description": "Provides some compatibility helpers for Flake8 plugins that intend to support Flake8 2.x and 3.x simultaneously." }, + { "name": "flaky", "uri": "https://anaconda.org/anaconda/flaky", "description": "Plugin for nose or py.test that automatically reruns flaky tests." }, + { "name": "flask", "uri": "https://anaconda.org/anaconda/flask", "description": "A simple framework for building complex web applications." }, + { "name": "flask-admin", "uri": "https://anaconda.org/anaconda/flask-admin", "description": "Simple and extensible admin interface framework for Flask" }, + { "name": "flask-appbuilder", "uri": "https://anaconda.org/anaconda/flask-appbuilder", "description": "Simple and rapid application development framework, built on top of Flask." }, + { "name": "flask-apscheduler", "uri": "https://anaconda.org/anaconda/flask-apscheduler", "description": "Flask-APScheduler is a Flask extension which adds support for the APScheduler" }, + { "name": "flask-babel", "uri": "https://anaconda.org/anaconda/flask-babel", "description": "Adds i18n/l10n support to Flask applications" }, + { "name": "flask-bcrypt", "uri": "https://anaconda.org/anaconda/flask-bcrypt", "description": "Bcrypt hashing for Flask." }, + { "name": "flask-caching", "uri": "https://anaconda.org/anaconda/flask-caching", "description": "Adds caching support to your Flask application" }, + { "name": "flask-compress", "uri": "https://anaconda.org/anaconda/flask-compress", "description": "Compress responses in your Flask app with gzip." }, + { "name": "flask-json", "uri": "https://anaconda.org/anaconda/flask-json", "description": "Better JSON support for Flask" }, + { "name": "flask-jwt-extended", "uri": "https://anaconda.org/anaconda/flask-jwt-extended", "description": "A Flask JWT extension" }, + { "name": "flask-login", "uri": "https://anaconda.org/anaconda/flask-login", "description": "User session management for Flask" }, + { "name": "flask-openid", "uri": "https://anaconda.org/anaconda/flask-openid", "description": "OpenID support for Flask" }, + { "name": "flask-restful", "uri": "https://anaconda.org/anaconda/flask-restful", "description": "Simple framework for creating REST APIs" }, + { "name": "flask-restx", "uri": "https://anaconda.org/anaconda/flask-restx", "description": "Fully featured framework for fast, easy and documented API development with Flask" }, + { "name": "flask-session", "uri": "https://anaconda.org/anaconda/flask-session", "description": "Adds server-side session support to your Flask application" }, + { "name": "flask-socketio", "uri": "https://anaconda.org/anaconda/flask-socketio", "description": "Socket.IO integration for Flask applications" }, + { "name": "flask-sqlalchemy", "uri": "https://anaconda.org/anaconda/flask-sqlalchemy", "description": "Adds SQLAlchemy support to your Flask application" }, + { "name": "flask-swagger", "uri": "https://anaconda.org/anaconda/flask-swagger", "description": "Extract swagger specs from your flask project" }, + { "name": "flask-wtf", "uri": "https://anaconda.org/anaconda/flask-wtf", "description": "Simple integration of Flask and WTForms" }, + { "name": "flask_cors", "uri": "https://anaconda.org/anaconda/flask_cors", "description": "Cross Origin Resource Sharing ( CORS ) support for Flask" }, + { "name": "flatbuffers", "uri": "https://anaconda.org/anaconda/flatbuffers", "description": "FlatBuffers is an efficient cross platform serialization library." }, + { "name": "flit", "uri": "https://anaconda.org/anaconda/flit", "description": "Simplified packaging of Python modules" }, + { "name": "flit-core", "uri": "https://anaconda.org/anaconda/flit-core", "description": "Simplified packaging of Python modules" }, + { "name": "flit-scm", "uri": "https://anaconda.org/anaconda/flit-scm", "description": "A PEP 518 build backend that uses setuptools_scm to generate a version file\nfrom your version control system, then flit_core to build the package." }, + { "name": "flite", "uri": "https://anaconda.org/anaconda/flite", "description": "No Summary" }, + { "name": "flower", "uri": "https://anaconda.org/anaconda/flower", "description": "Celery Flower" }, + { "name": "fmt", "uri": "https://anaconda.org/anaconda/fmt", "description": "{fmt} is an open-source formatting library for C++" }, + { "name": "folium", "uri": "https://anaconda.org/anaconda/folium", "description": "Make beautiful maps with Leaflet.js and Python" }, + { "name": "font-ttf-inconsolata", "uri": "https://anaconda.org/anaconda/font-ttf-inconsolata", "description": "Monospace font for pretty code listings" }, + { "name": "fontconfig", "uri": "https://anaconda.org/anaconda/fontconfig", "description": "A library for configuring and customizing font access." }, + { "name": "fontconfig-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/fontconfig-amzn2-aarch64", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-cos6-x86_64", "uri": "https://anaconda.org/anaconda/fontconfig-cos6-x86_64", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/fontconfig-cos7-ppc64le", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/fontconfig-devel-amzn2-aarch64", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/fontconfig-devel-cos6-i686", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/fontconfig-devel-cos6-x86_64", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontconfig-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/fontconfig-devel-cos7-ppc64le", "description": "(CDT) Font configuration and customization library" }, + { "name": "fontpackages-filesystem-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/fontpackages-filesystem-amzn2-aarch64", "description": "(CDT) Directories used by font packages" }, + { "name": "fonts-anaconda", "uri": "https://anaconda.org/anaconda/fonts-anaconda", "description": "No Summary" }, + { "name": "fonts-conda-ecosystem", "uri": "https://anaconda.org/anaconda/fonts-conda-ecosystem", "description": "Meta package pointing to the ecosystem specific font package" }, + { "name": "fonttools", "uri": "https://anaconda.org/anaconda/fonttools", "description": "fontTools is a library for manipulating fonts, written in Python." }, + { "name": "formulaic", "uri": "https://anaconda.org/anaconda/formulaic", "description": "A high-performance implementation of Wilkinson formulas for Python." }, + { "name": "freeglut", "uri": "https://anaconda.org/anaconda/freeglut", "description": "A GUI based on OpenGL." }, + { "name": "freetds", "uri": "https://anaconda.org/anaconda/freetds", "description": "FreeTDS is a free implementation of Sybase's DB-Library, CT-Library, and ODBC libraries" }, + { "name": "freetype-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/freetype-amzn2-aarch64", "description": "(CDT) A free and portable font rendering engine" }, + { "name": "freetype-cos6-i686", "uri": "https://anaconda.org/anaconda/freetype-cos6-i686", "description": "(CDT) A free and portable font rendering engine" }, + { "name": "freetype-cos6-x86_64", "uri": "https://anaconda.org/anaconda/freetype-cos6-x86_64", "description": "(CDT) A free and portable font rendering engine" }, + { "name": "freetype-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/freetype-cos7-ppc64le", "description": "(CDT) A free and portable font rendering engine" }, + { "name": "freetype-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/freetype-devel-amzn2-aarch64", "description": "(CDT) FreeType development libraries and header files" }, + { "name": "freetype-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/freetype-devel-cos6-i686", "description": "(CDT) FreeType development libraries and header files" }, + { "name": "freetype-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/freetype-devel-cos6-x86_64", "description": "(CDT) FreeType development libraries and header files" }, + { "name": "freetype-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/freetype-devel-cos7-ppc64le", "description": "(CDT) FreeType development libraries and header files" }, + { "name": "freetype-py", "uri": "https://anaconda.org/anaconda/freetype-py", "description": "Python binding for the freetype library" }, + { "name": "freexl", "uri": "https://anaconda.org/anaconda/freexl", "description": "Extract valid data from within Spreadsheets." }, + { "name": "freezegun", "uri": "https://anaconda.org/anaconda/freezegun", "description": "Let your Python tests travel through time" }, + { "name": "fribidi", "uri": "https://anaconda.org/anaconda/fribidi", "description": "The Free Implementation of the Unicode Bidirectional Algorithm." }, + { "name": "fribidi-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/fribidi-amzn2-aarch64", "description": "(CDT) Library implementing the Unicode Bidirectional Algorithm" }, + { "name": "frozendict", "uri": "https://anaconda.org/anaconda/frozendict", "description": "An immutable dictionary" }, + { "name": "frozenlist", "uri": "https://anaconda.org/anaconda/frozenlist", "description": "A list-like structure which implements collections.abc.MutableSequence" }, + { "name": "fs", "uri": "https://anaconda.org/anaconda/fs", "description": "Filesystem abstraction layer for Python" }, + { "name": "fsspec", "uri": "https://anaconda.org/anaconda/fsspec", "description": "A specification for pythonic filesystems" }, + { "name": "fuel", "uri": "https://anaconda.org/anaconda/fuel", "description": "No Summary" }, + { "name": "fugue", "uri": "https://anaconda.org/anaconda/fugue", "description": "An abstraction layer for distributed computation" }, + { "name": "fugue-sql-antlr", "uri": "https://anaconda.org/anaconda/fugue-sql-antlr", "description": "Fugue SQL Antlr Parser" }, + { "name": "func_timeout", "uri": "https://anaconda.org/anaconda/func_timeout", "description": "Python module to support running any existing function with a given timeout." }, + { "name": "furl", "uri": "https://anaconda.org/anaconda/furl", "description": "URL manipulation made simple." }, + { "name": "future", "uri": "https://anaconda.org/anaconda/future", "description": "Clean single-source support for Python 3 and 2" }, + { "name": "fuzzywuzzy", "uri": "https://anaconda.org/anaconda/fuzzywuzzy", "description": "Fuzzy string matching in python" }, + { "name": "fzf", "uri": "https://anaconda.org/anaconda/fzf", "description": "A command-line fuzzy finder" }, + { "name": "g2clib", "uri": "https://anaconda.org/anaconda/g2clib", "description": "C decoder/encoder routines for GRIB edition 2." }, + { "name": "gast", "uri": "https://anaconda.org/anaconda/gast", "description": "A generic AST to represent Python2 and Python3's Abstract Syntax Tree(AST)." }, + { "name": "gawk", "uri": "https://anaconda.org/anaconda/gawk", "description": "The awk utility interprets a special-purpose programming language that\nmakes it easy to handle simple data-reformatting jobs." }, + { "name": "gawk-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gawk-amzn2-aarch64", "description": "(CDT) The GNU version of the awk text processing utility" }, + { "name": "gcab", "uri": "https://anaconda.org/anaconda/gcab", "description": "A GObject library to create cabinet files" }, + { "name": "gcc-dbg_linux-32", "uri": "https://anaconda.org/anaconda/gcc-dbg_linux-32", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc-dbg_linux-64", "uri": "https://anaconda.org/anaconda/gcc-dbg_linux-64", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc-dbg_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gcc-dbg_linux-ppc64le", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc_bootstrap_linux-64", "uri": "https://anaconda.org/anaconda/gcc_bootstrap_linux-64", "description": "GCC bootstrap compilers for building deps" }, + { "name": "gcc_bootstrap_linux-aarch64", "uri": "https://anaconda.org/anaconda/gcc_bootstrap_linux-aarch64", "description": "GCC bootstrap compilers for building deps" }, + { "name": "gcc_bootstrap_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gcc_bootstrap_linux-ppc64le", "description": "GCC bootstrap compilers for building deps" }, + { "name": "gcc_bootstrap_linux-s390x", "uri": "https://anaconda.org/anaconda/gcc_bootstrap_linux-s390x", "description": "GCC bootstrap compilers for building deps" }, + { "name": "gcc_impl_linux-32", "uri": "https://anaconda.org/anaconda/gcc_impl_linux-32", "description": "GNU C Compiler" }, + { "name": "gcc_impl_linux-64", "uri": "https://anaconda.org/anaconda/gcc_impl_linux-64", "description": "GNU C Compiler" }, + { "name": "gcc_impl_linux-aarch64", "uri": "https://anaconda.org/anaconda/gcc_impl_linux-aarch64", "description": "GNU C Compiler" }, + { "name": "gcc_impl_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gcc_impl_linux-ppc64le", "description": "GNU C Compiler" }, + { "name": "gcc_impl_linux-s390x", "uri": "https://anaconda.org/anaconda/gcc_impl_linux-s390x", "description": "GNU C Compiler" }, + { "name": "gcc_linux-32", "uri": "https://anaconda.org/anaconda/gcc_linux-32", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc_linux-64", "uri": "https://anaconda.org/anaconda/gcc_linux-64", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc_linux-aarch64", "uri": "https://anaconda.org/anaconda/gcc_linux-aarch64", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gcc_linux-ppc64le", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gcc_linux-s390x", "uri": "https://anaconda.org/anaconda/gcc_linux-s390x", "description": "GNU C Compiler (activation scripts)" }, + { "name": "gconf2-cos6-i686", "uri": "https://anaconda.org/anaconda/gconf2-cos6-i686", "description": "(CDT) A process-transparent configuration system" }, + { "name": "gconf2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gconf2-cos6-x86_64", "description": "(CDT) A process-transparent configuration system" }, + { "name": "gconf2-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gconf2-cos7-ppc64le", "description": "(CDT) A process-transparent configuration system" }, + { "name": "gdal", "uri": "https://anaconda.org/anaconda/gdal", "description": "The Geospatial Data Abstraction Library (GDAL)" }, + { "name": "gdb", "uri": "https://anaconda.org/anaconda/gdb", "description": "The GNU Project Debugger" }, + { "name": "gdb-pretty-printer", "uri": "https://anaconda.org/anaconda/gdb-pretty-printer", "description": "GNU Compiler Collection Python Pretty Printers" }, + { "name": "gdb_linux-32", "uri": "https://anaconda.org/anaconda/gdb_linux-32", "description": "The GNU Project Debugger" }, + { "name": "gdb_linux-64", "uri": "https://anaconda.org/anaconda/gdb_linux-64", "description": "The GNU Project Debugger" }, + { "name": "gdb_linux-aarch64", "uri": "https://anaconda.org/anaconda/gdb_linux-aarch64", "description": "The GNU Project Debugger" }, + { "name": "gdb_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gdb_linux-ppc64le", "description": "The GNU Project Debugger" }, + { "name": "gdb_linux-s390x", "uri": "https://anaconda.org/anaconda/gdb_linux-s390x", "description": "The GNU Project Debugger" }, + { "name": "gdb_server_linux-64", "uri": "https://anaconda.org/anaconda/gdb_server_linux-64", "description": "The GNU Project Debugger" }, + { "name": "gdb_server_linux-aarch64", "uri": "https://anaconda.org/anaconda/gdb_server_linux-aarch64", "description": "The GNU Project Debugger" }, + { "name": "gdb_server_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gdb_server_linux-ppc64le", "description": "The GNU Project Debugger" }, + { "name": "gdb_server_linux-s390x", "uri": "https://anaconda.org/anaconda/gdb_server_linux-s390x", "description": "The GNU Project Debugger" }, + { "name": "gdbm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gdbm-amzn2-aarch64", "description": "(CDT) A GNU set of database routines which use extensible hashing" }, + { "name": "gdk-pixbuf", "uri": "https://anaconda.org/anaconda/gdk-pixbuf", "description": "GdkPixbuf is a library for image loading and manipulation." }, + { "name": "gdk-pixbuf2-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gdk-pixbuf2-amzn2-aarch64", "description": "(CDT) An image loading library" }, + { "name": "gdk-pixbuf2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gdk-pixbuf2-cos6-x86_64", "description": "(CDT) An image loading library" }, + { "name": "gdk-pixbuf2-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gdk-pixbuf2-devel-amzn2-aarch64", "description": "(CDT) Development files for gdk-pixbuf" }, + { "name": "gdk-pixbuf2-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gdk-pixbuf2-devel-cos6-x86_64", "description": "(CDT) Development files for gdk-pixbuf" }, + { "name": "gds-tools", "uri": "https://anaconda.org/anaconda/gds-tools", "description": "Library for NVIDIA GPUDirect Storage" }, + { "name": "gensim", "uri": "https://anaconda.org/anaconda/gensim", "description": "Topic Modelling for Humans" }, + { "name": "genson", "uri": "https://anaconda.org/anaconda/genson", "description": "GenSON is a powerful, user-friendly JSON Schema generator." }, + { "name": "geoalchemy2", "uri": "https://anaconda.org/anaconda/geoalchemy2", "description": "Using SQLAlchemy with Spatial Databases" }, + { "name": "geographiclib", "uri": "https://anaconda.org/anaconda/geographiclib", "description": "The geodesic routines from GeographicLib" }, + { "name": "geopandas", "uri": "https://anaconda.org/anaconda/geopandas", "description": "Geographic pandas extensions" }, + { "name": "geopandas-base", "uri": "https://anaconda.org/anaconda/geopandas-base", "description": "Geographic pandas extensions" }, + { "name": "geopy", "uri": "https://anaconda.org/anaconda/geopy", "description": "Python Geocoding Toolbox." }, + { "name": "geos", "uri": "https://anaconda.org/anaconda/geos", "description": "Geometry Engine - Open Source" }, + { "name": "geotiff", "uri": "https://anaconda.org/anaconda/geotiff", "description": "TIFF based interchange format for georeferenced raster imagery" }, + { "name": "geoviews", "uri": "https://anaconda.org/anaconda/geoviews", "description": "GeoViews is a Python library that makes it easy to explore and visualize geographical, meteorological, and oceanographic datasets, such as those used in weather, climate, and remote sensing research." }, + { "name": "geoviews-core", "uri": "https://anaconda.org/anaconda/geoviews-core", "description": "GeoViews is a Python library that makes it easy to explore and visualize geographical, meteorological, and oceanographic datasets, such as those used in weather, climate, and remote sensing research." }, + { "name": "getopt-win32", "uri": "https://anaconda.org/anaconda/getopt-win32", "description": "A port of getopt for Visual C++" }, + { "name": "gettext", "uri": "https://anaconda.org/anaconda/gettext", "description": "Internationalization package" }, + { "name": "gettext-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gettext-amzn2-aarch64", "description": "(CDT) GNU libraries and utilities for producing multi-lingual messages" }, + { "name": "gettext-common-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gettext-common-devel-cos7-ppc64le", "description": "(CDT) Common development files for gettext" }, + { "name": "gettext-cos6-i686", "uri": "https://anaconda.org/anaconda/gettext-cos6-i686", "description": "(CDT) GNU libraries and utilities for producing multi-lingual messages" }, + { "name": "gettext-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gettext-cos6-x86_64", "description": "(CDT) GNU libraries and utilities for producing multi-lingual messages" }, + { "name": "gettext-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gettext-cos7-ppc64le", "description": "(CDT) GNU libraries and utilities for producing multi-lingual messages" }, + { "name": "gettext-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/gettext-devel-cos6-i686", "description": "(CDT) Development files for gettext" }, + { "name": "gettext-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gettext-devel-cos6-x86_64", "description": "(CDT) Development files for gettext" }, + { "name": "gettext-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gettext-devel-cos7-ppc64le", "description": "(CDT) Development files for gettext" }, + { "name": "gettext-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gettext-libs-amzn2-aarch64", "description": "(CDT) Libraries for gettext" }, + { "name": "gettext-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/gettext-libs-cos6-i686", "description": "(CDT) Libraries for gettext" }, + { "name": "gettext-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gettext-libs-cos6-x86_64", "description": "(CDT) Libraries for gettext" }, + { "name": "gettext-libs-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gettext-libs-cos7-ppc64le", "description": "(CDT) Libraries for gettext" }, + { "name": "gevent", "uri": "https://anaconda.org/anaconda/gevent", "description": "Coroutine-based network library" }, + { "name": "geventhttpclient", "uri": "https://anaconda.org/anaconda/geventhttpclient", "description": "A high performance, concurrent http client library for python with gevent" }, + { "name": "gflags", "uri": "https://anaconda.org/anaconda/gflags", "description": "A C++ library that implements commandline flags processing." }, + { "name": "gfortran", "uri": "https://anaconda.org/anaconda/gfortran", "description": "Fortran compiler from the GNU Compiler Collection" }, + { "name": "gfortran-dbg_linux-32", "uri": "https://anaconda.org/anaconda/gfortran-dbg_linux-32", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran-dbg_linux-64", "uri": "https://anaconda.org/anaconda/gfortran-dbg_linux-64", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran-dbg_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gfortran-dbg_linux-ppc64le", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_impl_linux-32", "uri": "https://anaconda.org/anaconda/gfortran_impl_linux-32", "description": "GNU Fortran Compiler" }, + { "name": "gfortran_impl_linux-64", "uri": "https://anaconda.org/anaconda/gfortran_impl_linux-64", "description": "GNU Fortran Compiler" }, + { "name": "gfortran_impl_linux-aarch64", "uri": "https://anaconda.org/anaconda/gfortran_impl_linux-aarch64", "description": "GNU Fortran Compiler" }, + { "name": "gfortran_impl_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gfortran_impl_linux-ppc64le", "description": "GNU Fortran Compiler" }, + { "name": "gfortran_impl_linux-s390x", "uri": "https://anaconda.org/anaconda/gfortran_impl_linux-s390x", "description": "GNU Fortran Compiler" }, + { "name": "gfortran_impl_osx-64", "uri": "https://anaconda.org/anaconda/gfortran_impl_osx-64", "description": "Fortran compiler and libraries from the GNU Compiler Collection" }, + { "name": "gfortran_impl_osx-arm64", "uri": "https://anaconda.org/anaconda/gfortran_impl_osx-arm64", "description": "Fortran compiler and libraries from the GNU Compiler Collection" }, + { "name": "gfortran_linux-32", "uri": "https://anaconda.org/anaconda/gfortran_linux-32", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_linux-64", "uri": "https://anaconda.org/anaconda/gfortran_linux-64", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_linux-aarch64", "uri": "https://anaconda.org/anaconda/gfortran_linux-aarch64", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gfortran_linux-ppc64le", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_linux-s390x", "uri": "https://anaconda.org/anaconda/gfortran_linux-s390x", "description": "GNU Fortran Compiler (activation scripts)" }, + { "name": "gfortran_osx-arm64", "uri": "https://anaconda.org/anaconda/gfortran_osx-arm64", "description": "Fortran compiler from the GNU Compiler Collection" }, + { "name": "ghc", "uri": "https://anaconda.org/anaconda/ghc", "description": "Glorious Glasgow Haskell Compilation System" }, + { "name": "gi-docgen", "uri": "https://anaconda.org/anaconda/gi-docgen", "description": "Documentation tool for GObject-based libraries" }, + { "name": "giddy", "uri": "https://anaconda.org/anaconda/giddy", "description": "GeospatIal Distribution DYnamics (giddy) in PySAL" }, + { "name": "giflib", "uri": "https://anaconda.org/anaconda/giflib", "description": "Library for reading and writing gif images" }, + { "name": "git", "uri": "https://anaconda.org/anaconda/git", "description": "distributed version control system" }, + { "name": "git-cola", "uri": "https://anaconda.org/anaconda/git-cola", "description": "No Summary" }, + { "name": "git-lfs", "uri": "https://anaconda.org/anaconda/git-lfs", "description": "Git extension for versioning large files" }, + { "name": "gitdb", "uri": "https://anaconda.org/anaconda/gitdb", "description": "Git Object Database" }, + { "name": "gitpython", "uri": "https://anaconda.org/anaconda/gitpython", "description": "GitPython is a python library used to interact with Git repositories." }, + { "name": "gl-manpages-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gl-manpages-amzn2-aarch64", "description": "(CDT) OpenGL manpages" }, + { "name": "gl2ps", "uri": "https://anaconda.org/anaconda/gl2ps", "description": "OpenGL to PostScript Printing Library" }, + { "name": "glew", "uri": "https://anaconda.org/anaconda/glew", "description": "The OpenGL Extension Wrangler Library" }, + { "name": "glib", "uri": "https://anaconda.org/anaconda/glib", "description": "Provides core application building blocks for libraries and applications written in C." }, + { "name": "glib-networking-cos6-i686", "uri": "https://anaconda.org/anaconda/glib-networking-cos6-i686", "description": "(CDT) Networking support for GLib" }, + { "name": "glib-networking-cos6-x86_64", "uri": "https://anaconda.org/anaconda/glib-networking-cos6-x86_64", "description": "(CDT) Networking support for GLib" }, + { "name": "glib-tools", "uri": "https://anaconda.org/anaconda/glib-tools", "description": "Provides core application building blocks for libraries and applications written in C." }, + { "name": "glib2-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glib2-amzn2-aarch64", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/glib2-cos7-ppc64le", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-cos7-s390x", "uri": "https://anaconda.org/anaconda/glib2-cos7-s390x", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glib2-devel-amzn2-aarch64", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/glib2-devel-cos6-i686", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/glib2-devel-cos6-x86_64", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/glib2-devel-cos7-ppc64le", "description": "(CDT) A library of handy utility functions" }, + { "name": "glib2-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/glib2-devel-cos7-s390x", "description": "(CDT) A library of handy utility functions" }, + { "name": "glibc-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glibc-amzn2-aarch64", "description": "(CDT) The GNU libc libraries" }, + { "name": "glibc-common-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glibc-common-amzn2-aarch64", "description": "(CDT) Common binaries and locale data for glibc" }, + { "name": "glibc-minimal-langpack-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glibc-minimal-langpack-amzn2-aarch64", "description": "(CDT) Minimal language packs for glibc." }, + { "name": "glibmm24-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glibmm24-amzn2-aarch64", "description": "(CDT) C++ interface for the GLib library" }, + { "name": "glibmm24-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/glibmm24-devel-amzn2-aarch64", "description": "(CDT) Headers for developing programs that will use glibmm24" }, + { "name": "glog", "uri": "https://anaconda.org/anaconda/glog", "description": "C++ implementation of the Google logging module." }, + { "name": "glpk", "uri": "https://anaconda.org/anaconda/glpk", "description": "GNU Linear Programming Kit" }, + { "name": "glue-core", "uri": "https://anaconda.org/anaconda/glue-core", "description": "Multi-dimensional linked data exploration" }, + { "name": "glue-vispy-viewers", "uri": "https://anaconda.org/anaconda/glue-vispy-viewers", "description": "3D viewers for Glue" }, + { "name": "gluonts", "uri": "https://anaconda.org/anaconda/gluonts", "description": "GluonTS is a Python toolkit for probabilistic time series modeling, built around Apache MXNet (incubating)." }, + { "name": "gmock", "uri": "https://anaconda.org/anaconda/gmock", "description": "Google's C++ test framework" }, + { "name": "gmp-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gmp-amzn2-aarch64", "description": "(CDT) A GNU arbitrary precision library" }, + { "name": "gmpy2", "uri": "https://anaconda.org/anaconda/gmpy2", "description": "GMP/MPIR, MPFR, and MPC interface to Python 2.6+ and 3.x" }, + { "name": "gn", "uri": "https://anaconda.org/anaconda/gn", "description": "GN is a meta-build system that generates build files for Ninja." }, + { "name": "gnuconfig", "uri": "https://anaconda.org/anaconda/gnuconfig", "description": "Updated config.sub and config.guess file from GNU" }, + { "name": "gnutls", "uri": "https://anaconda.org/anaconda/gnutls", "description": "GnuTLS is a secure communications library implementing the SSL, TLS and DTLS protocols" }, + { "name": "gnutls-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gnutls-amzn2-aarch64", "description": "(CDT) A TLS protocol implementation" }, + { "name": "gnutls-cos6-i686", "uri": "https://anaconda.org/anaconda/gnutls-cos6-i686", "description": "(CDT) A TLS protocol implementation" }, + { "name": "gnutls-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gnutls-cos6-x86_64", "description": "(CDT) A TLS protocol implementation" }, + { "name": "gnutls-cos7-s390x", "uri": "https://anaconda.org/anaconda/gnutls-cos7-s390x", "description": "(CDT) A TLS protocol implementation" }, + { "name": "go", "uri": "https://anaconda.org/anaconda/go", "description": "The Go Programming Language" }, + { "name": "go-cgo", "uri": "https://anaconda.org/anaconda/go-cgo", "description": "The Go Programming Language (cgo)" }, + { "name": "go-cgo_linux-64", "uri": "https://anaconda.org/anaconda/go-cgo_linux-64", "description": "The Go (cgo) compiler activation scripts for conda-build." }, + { "name": "go-cgo_linux-aarch64", "uri": "https://anaconda.org/anaconda/go-cgo_linux-aarch64", "description": "The Go (cgo) compiler activation scripts for conda-build." }, + { "name": "go-cgo_osx-64", "uri": "https://anaconda.org/anaconda/go-cgo_osx-64", "description": "The Go (cgo) compiler activation scripts for conda-build." }, + { "name": "go-cgo_osx-arm64", "uri": "https://anaconda.org/anaconda/go-cgo_osx-arm64", "description": "The Go (cgo) compiler activation scripts for conda-build." }, + { "name": "go-cgo_win-64", "uri": "https://anaconda.org/anaconda/go-cgo_win-64", "description": "The Go (cgo) compiler activation scripts for conda-build." }, + { "name": "go-core", "uri": "https://anaconda.org/anaconda/go-core", "description": "The Go Programming Language" }, + { "name": "go-licenses", "uri": "https://anaconda.org/anaconda/go-licenses", "description": "A tool to collect licenses from the dependency tree of a Go package in order to comply with redistribution terms." }, + { "name": "go-nocgo", "uri": "https://anaconda.org/anaconda/go-nocgo", "description": "The Go Programming Language (nocgo)" }, + { "name": "go-nocgo_linux-64", "uri": "https://anaconda.org/anaconda/go-nocgo_linux-64", "description": "The Go (nocgo) compiler activation scripts for conda-build." }, + { "name": "go-nocgo_linux-aarch64", "uri": "https://anaconda.org/anaconda/go-nocgo_linux-aarch64", "description": "The Go (nocgo) compiler activation scripts for conda-build." }, + { "name": "go-nocgo_osx-64", "uri": "https://anaconda.org/anaconda/go-nocgo_osx-64", "description": "The Go (nocgo) compiler activation scripts for conda-build." }, + { "name": "go-nocgo_osx-arm64", "uri": "https://anaconda.org/anaconda/go-nocgo_osx-arm64", "description": "The Go (nocgo) compiler activation scripts for conda-build." }, + { "name": "go-nocgo_win-64", "uri": "https://anaconda.org/anaconda/go-nocgo_win-64", "description": "The Go (nocgo) compiler activation scripts for conda-build." }, + { "name": "go_linux-32", "uri": "https://anaconda.org/anaconda/go_linux-32", "description": "The Go Programming Language" }, + { "name": "go_linux-64", "uri": "https://anaconda.org/anaconda/go_linux-64", "description": "The Go Programming Language" }, + { "name": "go_linux-ppc64le", "uri": "https://anaconda.org/anaconda/go_linux-ppc64le", "description": "The Go Programming Language" }, + { "name": "go_osx-64", "uri": "https://anaconda.org/anaconda/go_osx-64", "description": "The Go Programming Language" }, + { "name": "go_win-32", "uri": "https://anaconda.org/anaconda/go_win-32", "description": "The Go Programming Language" }, + { "name": "go_win-64", "uri": "https://anaconda.org/anaconda/go_win-64", "description": "The Go Programming Language" }, + { "name": "gobject-introspection", "uri": "https://anaconda.org/anaconda/gobject-introspection", "description": "Middleware for binding GObject-based code to other languages." }, + { "name": "google-api-core", "uri": "https://anaconda.org/anaconda/google-api-core", "description": "Core Library for Google Client Libraries" }, + { "name": "google-api-core-grpc", "uri": "https://anaconda.org/anaconda/google-api-core-grpc", "description": "Core Library for Google Client Libraries with grpc" }, + { "name": "google-api-core-grpcgcp", "uri": "https://anaconda.org/anaconda/google-api-core-grpcgcp", "description": "Core Library for Google Client Libraries with grpcio-gcp" }, + { "name": "google-api-core-grpcio-gcp", "uri": "https://anaconda.org/anaconda/google-api-core-grpcio-gcp", "description": "Core Library for Google Client Libraries with grpcio-gcp" }, + { "name": "google-auth", "uri": "https://anaconda.org/anaconda/google-auth", "description": "Google authentication library for Python" }, + { "name": "google-auth-oauthlib", "uri": "https://anaconda.org/anaconda/google-auth-oauthlib", "description": "Google Authentication Library, oauthlib integration with google-auth" }, + { "name": "google-cloud-core", "uri": "https://anaconda.org/anaconda/google-cloud-core", "description": "API Client library for Google Cloud: Core Helpers" }, + { "name": "google-cloud-storage", "uri": "https://anaconda.org/anaconda/google-cloud-storage", "description": "Python Client for Google Cloud Storage" }, + { "name": "google-crc32c", "uri": "https://anaconda.org/anaconda/google-crc32c", "description": "Python wrapper for a hardware-based implementation of the CRC32C hashing algorithm" }, + { "name": "google-pasta", "uri": "https://anaconda.org/anaconda/google-pasta", "description": "pasta is an AST-based Python refactoring library" }, + { "name": "google-resumable-media", "uri": "https://anaconda.org/anaconda/google-resumable-media", "description": "Utilities for Google Media Downloads and Resumable Uploads" }, + { "name": "googleapis-common-protos", "uri": "https://anaconda.org/anaconda/googleapis-common-protos", "description": "Common protobufs used in Google APIs" }, + { "name": "googleapis-common-protos-grpc", "uri": "https://anaconda.org/anaconda/googleapis-common-protos-grpc", "description": "Extra grpc requirements for googleapis-common-protos" }, + { "name": "gperf", "uri": "https://anaconda.org/anaconda/gperf", "description": "GNU gperf is a perfect hash function generator." }, + { "name": "gperf-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gperf-cos6-x86_64", "description": "(CDT) A perfect hash function generator" }, + { "name": "gperf-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gperf-cos7-ppc64le", "description": "(CDT) A perfect hash function generator" }, + { "name": "gptcache", "uri": "https://anaconda.org/anaconda/gptcache", "description": "GPTCache is a project dedicated to building a semantic cache for storing LLM responses." }, + { "name": "gpustat", "uri": "https://anaconda.org/anaconda/gpustat", "description": "A simple command-line utility for querying and monitoring GPU status." }, + { "name": "graphene", "uri": "https://anaconda.org/anaconda/graphene", "description": "GraphQL Framework for Python" }, + { "name": "graphite2", "uri": "https://anaconda.org/anaconda/graphite2", "description": "A \"smart font\" system that handles the complexities of lesser-known languages of the world." }, + { "name": "graphite2-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/graphite2-amzn2-aarch64", "description": "(CDT) Font rendering capabilities for complex non-Roman writing systems" }, + { "name": "graphlib-backport", "uri": "https://anaconda.org/anaconda/graphlib-backport", "description": "Backport of the Python 3.9 graphlib module for Python 3.6+" }, + { "name": "graphql-core", "uri": "https://anaconda.org/anaconda/graphql-core", "description": "A Python 3.6+ port of the GraphQL.js reference implementation of GraphQL." }, + { "name": "graphql-relay", "uri": "https://anaconda.org/anaconda/graphql-relay", "description": "Relay library for graphql-core" }, + { "name": "graphviz", "uri": "https://anaconda.org/anaconda/graphviz", "description": "Open Source graph visualization software." }, + { "name": "grayskull", "uri": "https://anaconda.org/anaconda/grayskull", "description": "Project to generate recipes for conda." }, + { "name": "greenlet", "uri": "https://anaconda.org/anaconda/greenlet", "description": "Lightweight in-process concurrent programming" }, + { "name": "grep-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/grep-amzn2-aarch64", "description": "(CDT) Pattern matching utilities" }, + { "name": "greyskull", "uri": "https://anaconda.org/anaconda/greyskull", "description": "Project to generate recipes for conda." }, + { "name": "groff", "uri": "https://anaconda.org/anaconda/groff", "description": "Groff (GNU troff) is a typesetting system" }, + { "name": "grpc-cpp", "uri": "https://anaconda.org/anaconda/grpc-cpp", "description": "gRPC - A high-performance, open-source universal RPC framework" }, + { "name": "grpcio", "uri": "https://anaconda.org/anaconda/grpcio", "description": "gRPC - A high-performance, open-source universal RPC framework" }, + { "name": "grpcio-gcp", "uri": "https://anaconda.org/anaconda/grpcio-gcp", "description": "gRPC extensions for Google Cloud Platform" }, + { "name": "grpcio-status", "uri": "https://anaconda.org/anaconda/grpcio-status", "description": "Status proto mapping for gRPC" }, + { "name": "grpcio-tools", "uri": "https://anaconda.org/anaconda/grpcio-tools", "description": "Protobuf code generator for gRPC" }, + { "name": "gsl", "uri": "https://anaconda.org/anaconda/gsl", "description": "GNU Scientific Library" }, + { "name": "gst-plugins-base", "uri": "https://anaconda.org/anaconda/gst-plugins-base", "description": "GStreamer Base Plug-ins" }, + { "name": "gst-plugins-good", "uri": "https://anaconda.org/anaconda/gst-plugins-good", "description": "GStreamer Good Plug-ins" }, + { "name": "gstreamer", "uri": "https://anaconda.org/anaconda/gstreamer", "description": "Library for constructing graphs of media-handling components" }, + { "name": "gtest", "uri": "https://anaconda.org/anaconda/gtest", "description": "Google's C++ test framework" }, + { "name": "gtk-update-icon-cache-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gtk-update-icon-cache-amzn2-aarch64", "description": "(CDT) Icon theme caching utility" }, + { "name": "gtk2", "uri": "https://anaconda.org/anaconda/gtk2", "description": "Primary library used to construct user interfaces in GNOME applications" }, + { "name": "gtk2-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gtk2-amzn2-aarch64", "description": "(CDT) The GIMP ToolKit (GTK+), a library for creating GUIs for X" }, + { "name": "gtk2-cos6-i686", "uri": "https://anaconda.org/anaconda/gtk2-cos6-i686", "description": "(CDT) The GIMP ToolKit (GTK+), a library for creating GUIs for X" }, + { "name": "gtk2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gtk2-cos6-x86_64", "description": "(CDT) The GIMP ToolKit (GTK+), a library for creating GUIs for X" }, + { "name": "gtk2-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gtk2-cos7-ppc64le", "description": "(CDT) The GIMP ToolKit (GTK+), a library for creating GUIs for X" }, + { "name": "gtk2-cos7-s390x", "uri": "https://anaconda.org/anaconda/gtk2-cos7-s390x", "description": "(CDT) The GIMP ToolKit (GTK+), a library for creating GUIs for X" }, + { "name": "gtk2-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gtk2-devel-amzn2-aarch64", "description": "(CDT) Development files for GTK+" }, + { "name": "gtk2-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/gtk2-devel-cos6-i686", "description": "(CDT) Development files for GTK+" }, + { "name": "gtk2-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/gtk2-devel-cos7-ppc64le", "description": "(CDT) Development files for GTK+" }, + { "name": "gtk2-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/gtk2-devel-cos7-s390x", "description": "(CDT) Development files for GTK+" }, + { "name": "gtk3", "uri": "https://anaconda.org/anaconda/gtk3", "description": "Version 3 of the Gtk+ graphical toolkit" }, + { "name": "gtkmm24-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gtkmm24-amzn2-aarch64", "description": "(CDT) C++ interface for GTK2 (a GUI library for X)" }, + { "name": "gtkmm24-cos6-x86_64", "uri": "https://anaconda.org/anaconda/gtkmm24-cos6-x86_64", "description": "(CDT) C++ interface for GTK2 (a GUI library for X)" }, + { "name": "gtkmm24-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gtkmm24-devel-amzn2-aarch64", "description": "(CDT) Headers for developing programs that will use gtkmm24." }, + { "name": "gtkmm24-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/gtkmm24-devel-cos6-i686", "description": "(CDT) Headers for developing programs that will use gtkmm24." }, + { "name": "gtkmm24-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/gtkmm24-devel-cos7-s390x", "description": "(CDT) Headers for developing programs that will use gtkmm24." }, + { "name": "gts", "uri": "https://anaconda.org/anaconda/gts", "description": "GNU Triangulated Surface Library" }, + { "name": "gunicorn", "uri": "https://anaconda.org/anaconda/gunicorn", "description": "WSGI HTTP Server for UNIX" }, + { "name": "gxx-dbg_linux-32", "uri": "https://anaconda.org/anaconda/gxx-dbg_linux-32", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx-dbg_linux-64", "uri": "https://anaconda.org/anaconda/gxx-dbg_linux-64", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx-dbg_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gxx-dbg_linux-ppc64le", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx_impl_linux-32", "uri": "https://anaconda.org/anaconda/gxx_impl_linux-32", "description": "GNU C++ Compiler" }, + { "name": "gxx_impl_linux-64", "uri": "https://anaconda.org/anaconda/gxx_impl_linux-64", "description": "GNU C++ Compiler" }, + { "name": "gxx_impl_linux-aarch64", "uri": "https://anaconda.org/anaconda/gxx_impl_linux-aarch64", "description": "GNU C++ Compiler" }, + { "name": "gxx_impl_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gxx_impl_linux-ppc64le", "description": "GNU C++ Compiler" }, + { "name": "gxx_impl_linux-s390x", "uri": "https://anaconda.org/anaconda/gxx_impl_linux-s390x", "description": "GNU C++ Compiler" }, + { "name": "gxx_linux-32", "uri": "https://anaconda.org/anaconda/gxx_linux-32", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx_linux-64", "uri": "https://anaconda.org/anaconda/gxx_linux-64", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx_linux-aarch64", "uri": "https://anaconda.org/anaconda/gxx_linux-aarch64", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx_linux-ppc64le", "uri": "https://anaconda.org/anaconda/gxx_linux-ppc64le", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gxx_linux-s390x", "uri": "https://anaconda.org/anaconda/gxx_linux-s390x", "description": "GNU C++ Compiler (activation scripts)" }, + { "name": "gymnasium", "uri": "https://anaconda.org/anaconda/gymnasium", "description": "A standard API for reinforcement learning and a diverse set of reference environments (formerly Gym)" }, + { "name": "gymnasium-notices", "uri": "https://anaconda.org/anaconda/gymnasium-notices", "description": "Notices for gymnasium" }, + { "name": "gzip-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/gzip-amzn2-aarch64", "description": "(CDT) The GNU data compression program" }, + { "name": "h11", "uri": "https://anaconda.org/anaconda/h11", "description": "A pure-Python HTTP/1.1 protocol library." }, + { "name": "h2", "uri": "https://anaconda.org/anaconda/h2", "description": "HTTP/2 State-Machine based protocol implementation" }, + { "name": "h5netcdf", "uri": "https://anaconda.org/anaconda/h5netcdf", "description": "Pythonic interface to netCDF4 via h5py" }, + { "name": "h5py", "uri": "https://anaconda.org/anaconda/h5py", "description": "Read and write HDF5 files from Python" }, + { "name": "harfbuzz", "uri": "https://anaconda.org/anaconda/harfbuzz", "description": "A text shaping library." }, + { "name": "harfbuzz-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/harfbuzz-amzn2-aarch64", "description": "(CDT) Text shaping library" }, + { "name": "harfbuzz-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/harfbuzz-cos7-ppc64le", "description": "(CDT) Text shaping library" }, + { "name": "hatch-fancy-pypi-readme", "uri": "https://anaconda.org/anaconda/hatch-fancy-pypi-readme", "description": "Fancy PyPI READMEs with Hatch" }, + { "name": "hatch-jupyter-builder", "uri": "https://anaconda.org/anaconda/hatch-jupyter-builder", "description": "A hatch plugin to help build Jupyter packages" }, + { "name": "hatch-nodejs-version", "uri": "https://anaconda.org/anaconda/hatch-nodejs-version", "description": "Hatch plugin for versioning from a package.json file" }, + { "name": "hatch-requirements-txt", "uri": "https://anaconda.org/anaconda/hatch-requirements-txt", "description": "Hatchling plugin to read project dependencies from requirements.txt" }, + { "name": "hatch-vcs", "uri": "https://anaconda.org/anaconda/hatch-vcs", "description": "Hatch plugin for versioning with your preferred VCS" }, + { "name": "hatchling", "uri": "https://anaconda.org/anaconda/hatchling", "description": "Modern, extensible Python build backend" }, + { "name": "hdf5", "uri": "https://anaconda.org/anaconda/hdf5", "description": "HDF5 is a data model, library, and file format for storing and managing data" }, + { "name": "hdfeos2", "uri": "https://anaconda.org/anaconda/hdfeos2", "description": "Earth Observing System HDF." }, + { "name": "hdfs3", "uri": "https://anaconda.org/anaconda/hdfs3", "description": "Python wrapper for libhdfs3" }, + { "name": "hdijupyterutils", "uri": "https://anaconda.org/anaconda/hdijupyterutils", "description": "Project with useful classes/methods for all projects created by the HDInsight team at Microsoft around Jupyter" }, + { "name": "hdmedians", "uri": "https://anaconda.org/anaconda/hdmedians", "description": "High-dimensional medians" }, + { "name": "help2man", "uri": "https://anaconda.org/anaconda/help2man", "description": "help2man produces simple manual pages from the --help and --version output of other commands." }, + { "name": "hicolor-icon-theme", "uri": "https://anaconda.org/anaconda/hicolor-icon-theme", "description": "Fallback theme for FreeDesktop.org icon themes" }, + { "name": "hicolor-icon-theme-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/hicolor-icon-theme-amzn2-aarch64", "description": "(CDT) Basic requirement for icon themes" }, + { "name": "hidapi", "uri": "https://anaconda.org/anaconda/hidapi", "description": "Simple lib for communicating with USB and Bluetooth HID devices" }, + { "name": "hijri-converter", "uri": "https://anaconda.org/anaconda/hijri-converter", "description": "Accurate Hijri-Gregorian date converter based on the Umm al-Qura calendar" }, + { "name": "hiredis", "uri": "https://anaconda.org/anaconda/hiredis", "description": "Python wrapper for hiredis" }, + { "name": "holidays", "uri": "https://anaconda.org/anaconda/holidays", "description": "Generate and work with holidays in Python" }, + { "name": "hologram", "uri": "https://anaconda.org/anaconda/hologram", "description": "JSON schema generation from dataclasses" }, + { "name": "holoviews", "uri": "https://anaconda.org/anaconda/holoviews", "description": "Stop plotting your data - annotate your data and let it visualize itself." }, + { "name": "hpack", "uri": "https://anaconda.org/anaconda/hpack", "description": "HTTP/2 Header Encoding for Python" }, + { "name": "hsluv", "uri": "https://anaconda.org/anaconda/hsluv", "description": "A Python implementation of HSLuv (revision 4)." }, + { "name": "hstspreload", "uri": "https://anaconda.org/anaconda/hstspreload", "description": "Chromium HSTS Preload list as a Python package and updated daily." }, + { "name": "htbuilder", "uri": "https://anaconda.org/anaconda/htbuilder", "description": "A purely-functional HTML builder for Python. Think JSX rather than templates." }, + { "name": "htmlmin", "uri": "https://anaconda.org/anaconda/htmlmin", "description": "A configurable HTML Minifier with safety features" }, + { "name": "htslib", "uri": "https://anaconda.org/anaconda/htslib", "description": "C library for high-throughput sequencing data formats." }, + { "name": "httpcore", "uri": "https://anaconda.org/anaconda/httpcore", "description": "The next generation HTTP client." }, + { "name": "httptools", "uri": "https://anaconda.org/anaconda/httptools", "description": "Fast HTTP parser" }, + { "name": "httpx", "uri": "https://anaconda.org/anaconda/httpx", "description": "A next-generation HTTP client for Python." }, + { "name": "httpx-sse", "uri": "https://anaconda.org/anaconda/httpx-sse", "description": "Consume Server-Sent Event (SSE) messages with HTTPX." }, + { "name": "huggingface_accelerate", "uri": "https://anaconda.org/anaconda/huggingface_accelerate", "description": "Training loop of PyTorch without boilerplate code" }, + { "name": "huggingface_hub", "uri": "https://anaconda.org/anaconda/huggingface_hub", "description": "Client library to download and publish models, datasets and other repos on the huggingface.co hub" }, + { "name": "humanfriendly", "uri": "https://anaconda.org/anaconda/humanfriendly", "description": "Human friendly output for text interfaces using Python." }, + { "name": "humanize", "uri": "https://anaconda.org/anaconda/humanize", "description": "Python humanize utilities" }, + { "name": "hupper", "uri": "https://anaconda.org/anaconda/hupper", "description": "Integrated process monitor for developing and reloading daemons." }, + { "name": "hvplot", "uri": "https://anaconda.org/anaconda/hvplot", "description": "A high-level plotting API for the PyData ecosystem built on HoloViews" }, + { "name": "hwdata-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/hwdata-amzn2-aarch64", "description": "(CDT) Hardware identification and configuration data" }, + { "name": "hyperframe", "uri": "https://anaconda.org/anaconda/hyperframe", "description": "Pure-Python HTTP/2 framing" }, + { "name": "hypothesis", "uri": "https://anaconda.org/anaconda/hypothesis", "description": "A library for property based testing" }, + { "name": "ibis-framework", "uri": "https://anaconda.org/anaconda/ibis-framework", "description": "Productivity-centric Python Big Data Framework" }, + { "name": "icalendar", "uri": "https://anaconda.org/anaconda/icalendar", "description": "iCalendar parser/generator" }, + { "name": "icu", "uri": "https://anaconda.org/anaconda/icu", "description": "International Components for Unicode." }, + { "name": "identify", "uri": "https://anaconda.org/anaconda/identify", "description": "File identification library for Python" }, + { "name": "idna", "uri": "https://anaconda.org/anaconda/idna", "description": "Internationalized Domain Names in Applications (IDNA)." }, + { "name": "idna_ssl", "uri": "https://anaconda.org/anaconda/idna_ssl", "description": "Patch ssl.match_hostname for Unicode(idna) domains support" }, + { "name": "ijson", "uri": "https://anaconda.org/anaconda/ijson", "description": "Ijson is an iterative JSON parser with a standard Python iterator interface." }, + { "name": "imagecodecs", "uri": "https://anaconda.org/anaconda/imagecodecs", "description": "Image transformation, compression, and decompression codecs" }, + { "name": "imagehash", "uri": "https://anaconda.org/anaconda/imagehash", "description": "A Python Perceptual Image Hahsing Module" }, + { "name": "imageio", "uri": "https://anaconda.org/anaconda/imageio", "description": "A Python library for reading and writing image data" }, + { "name": "imagesize", "uri": "https://anaconda.org/anaconda/imagesize", "description": "Getting image size from png/jpeg/jpeg2000/gif file" }, + { "name": "imbalanced-learn", "uri": "https://anaconda.org/anaconda/imbalanced-learn", "description": "Python module to balance data set using under- and over-sampling" }, + { "name": "imgaug", "uri": "https://anaconda.org/anaconda/imgaug", "description": "Image augmentation for machine learning experiments" }, + { "name": "iminuit", "uri": "https://anaconda.org/anaconda/iminuit", "description": "Interactive Minimization Tools based on MINUIT" }, + { "name": "immutables", "uri": "https://anaconda.org/anaconda/immutables", "description": "Immutable Collections" }, + { "name": "importlib-metadata", "uri": "https://anaconda.org/anaconda/importlib-metadata", "description": "A library to access the metadata for a Python package." }, + { "name": "importlib-resources", "uri": "https://anaconda.org/anaconda/importlib-resources", "description": "Backport of Python 3.7's standard library `importlib.resources`" }, + { "name": "importlib_metadata", "uri": "https://anaconda.org/anaconda/importlib_metadata", "description": "A library to access the metadata for a Python package." }, + { "name": "importlib_resources", "uri": "https://anaconda.org/anaconda/importlib_resources", "description": "Backport of Python 3.7's standard library `importlib.resources`" }, + { "name": "incremental", "uri": "https://anaconda.org/anaconda/incremental", "description": "Incremental is a small library that versions your Python projects." }, + { "name": "inequality", "uri": "https://anaconda.org/anaconda/inequality", "description": "Spatial inequality analysis for PySAL A library of spatial analysis functions." }, + { "name": "infinity", "uri": "https://anaconda.org/anaconda/infinity", "description": "All-in-one infinity value for Python. Can be compared to any object." }, + { "name": "inflate64", "uri": "https://anaconda.org/anaconda/inflate64", "description": "deflate64 compression/decompression library" }, + { "name": "inflect", "uri": "https://anaconda.org/anaconda/inflect", "description": "Correctly generate plurals, singular nouns, ordinals, indefinite articles; convert numbers to words" }, + { "name": "inflection", "uri": "https://anaconda.org/anaconda/inflection", "description": "A port of Ruby on Rails inflector to Python" }, + { "name": "info-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/info-amzn2-aarch64", "description": "(CDT) A stand-alone TTY-based reader for GNU texinfo documentation" }, + { "name": "iniconfig", "uri": "https://anaconda.org/anaconda/iniconfig", "description": "iniconfig: brain-dead simple config-ini parsing" }, + { "name": "inquirer", "uri": "https://anaconda.org/anaconda/inquirer", "description": "Collection of common interactive command line user interfaces, based on Inquirer.js" }, + { "name": "intake", "uri": "https://anaconda.org/anaconda/intake", "description": "Data load and catalog system" }, + { "name": "intake-parquet", "uri": "https://anaconda.org/anaconda/intake-parquet", "description": "Intake parquet plugin" }, + { "name": "intake-xarray", "uri": "https://anaconda.org/anaconda/intake-xarray", "description": "xarray plugins for Intake" }, + { "name": "intel-cmplr-lib-rt", "uri": "https://anaconda.org/anaconda/intel-cmplr-lib-rt", "description": "Runtime for Intel® C++ Compiler Classic" }, + { "name": "intel-cmplr-lic-rt", "uri": "https://anaconda.org/anaconda/intel-cmplr-lic-rt", "description": "Intel End User License Agreement for Developer Tools" }, + { "name": "intel-extension-for-pytorch", "uri": "https://anaconda.org/anaconda/intel-extension-for-pytorch", "description": "Intel® Extension for PyTorch for extra performance boost on Intel hardware." }, + { "name": "intel-fortran-rt", "uri": "https://anaconda.org/anaconda/intel-fortran-rt", "description": "Runtime for Intel® Fortran Compiler Classic and Intel® Fortran Compiler (Beta)" }, + { "name": "intel-fortran_win-32", "uri": "https://anaconda.org/anaconda/intel-fortran_win-32", "description": "Activation and version verification of Intel Fortran compiler" }, + { "name": "intel-fortran_win-64", "uri": "https://anaconda.org/anaconda/intel-fortran_win-64", "description": "Activation and version verification of Intel Fortran compiler" }, + { "name": "intel-opencl-rt", "uri": "https://anaconda.org/anaconda/intel-opencl-rt", "description": "Intel® CPU Runtime for OpenCL™" }, + { "name": "intel-openmp", "uri": "https://anaconda.org/anaconda/intel-openmp", "description": "Math library for Intel and compatible processors" }, + { "name": "interface_meta", "uri": "https://anaconda.org/anaconda/interface_meta", "description": "`interface_meta` provides a convenient way to expose an extensible API with enforced method signatures and consistent documentation." }, + { "name": "intervals", "uri": "https://anaconda.org/anaconda/intervals", "description": "Python tools for handling intervals (ranges of comparable objects)." }, + { "name": "intervaltree", "uri": "https://anaconda.org/anaconda/intervaltree", "description": "Editable interval tree data structure for Python 2 and 3" }, + { "name": "intreehooks", "uri": "https://anaconda.org/anaconda/intreehooks", "description": "Load a PEP 517 backend from inside the source tree" }, + { "name": "invoke", "uri": "https://anaconda.org/anaconda/invoke", "description": "Pythonic task execution" }, + { "name": "ipaddr", "uri": "https://anaconda.org/anaconda/ipaddr", "description": "Google's Python IP address manipulation library" }, + { "name": "ipykernel", "uri": "https://anaconda.org/anaconda/ipykernel", "description": "IPython Kernel for Jupyter" }, + { "name": "ipyleaflet", "uri": "https://anaconda.org/anaconda/ipyleaflet", "description": "A Jupyter / Leaflet bridge enabling interactive maps in the Jupyter notebook." }, + { "name": "ipympl", "uri": "https://anaconda.org/anaconda/ipympl", "description": "Matplotlib Jupyter Extension" }, + { "name": "ipyparallel", "uri": "https://anaconda.org/anaconda/ipyparallel", "description": "Interactive Parallel Computing with IPython" }, + { "name": "ipython", "uri": "https://anaconda.org/anaconda/ipython", "description": "IPython: Productive Interactive Computing" }, + { "name": "ipython-sql", "uri": "https://anaconda.org/anaconda/ipython-sql", "description": "RDBMS access via IPython" }, + { "name": "ipywidgets", "uri": "https://anaconda.org/anaconda/ipywidgets", "description": "Interactive Widgets for the Jupyter Notebook" }, + { "name": "isa-l", "uri": "https://anaconda.org/anaconda/isa-l", "description": "provides tools to minimize disk space use and maximize storage throughput, security, and resilience." }, + { "name": "isodate", "uri": "https://anaconda.org/anaconda/isodate", "description": "An ISO 8601 date/time/duration parser and formatter." }, + { "name": "isort", "uri": "https://anaconda.org/anaconda/isort", "description": "A Python utility / library to sort Python imports." }, + { "name": "itemadapter", "uri": "https://anaconda.org/anaconda/itemadapter", "description": "Common interface for different data containers" }, + { "name": "itemloaders", "uri": "https://anaconda.org/anaconda/itemloaders", "description": "Collect data from HTML and XML sources" }, + { "name": "itsdangerous", "uri": "https://anaconda.org/anaconda/itsdangerous", "description": "Safely pass data to untrusted environments and back." }, + { "name": "jaeger-client", "uri": "https://anaconda.org/anaconda/jaeger-client", "description": "Jaeger Python OpenTracing Tracer implementation" }, + { "name": "jansson", "uri": "https://anaconda.org/anaconda/jansson", "description": "Jansson is a C library for encoding, decoding and manipulating JSON data." }, + { "name": "jaraco.classes", "uri": "https://anaconda.org/anaconda/jaraco.classes", "description": "jaraco.classes" }, + { "name": "jaraco.collections", "uri": "https://anaconda.org/anaconda/jaraco.collections", "description": "Models and classes to supplement the stdlib 'collections' module." }, + { "name": "jaraco.context", "uri": "https://anaconda.org/anaconda/jaraco.context", "description": "Context managers by jaraco" }, + { "name": "jaraco.functools", "uri": "https://anaconda.org/anaconda/jaraco.functools", "description": "Additional functools in the spirit of stdlib's functools." }, + { "name": "jaraco.itertools", "uri": "https://anaconda.org/anaconda/jaraco.itertools", "description": "Additional itertools in the spirit of stdlib's itertools." }, + { "name": "jaraco.test", "uri": "https://anaconda.org/anaconda/jaraco.test", "description": "Testing support by jaraco" }, + { "name": "jaraco.text", "uri": "https://anaconda.org/anaconda/jaraco.text", "description": "Module for text manipulation" }, + { "name": "jasper", "uri": "https://anaconda.org/anaconda/jasper", "description": "A reference implementation of the codec specified in the JPEG-2000 Part-1 standard." }, + { "name": "jasper-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/jasper-libs-amzn2-aarch64", "description": "(CDT) Runtime libraries for jasper" }, + { "name": "java-1.7.0-openjdk-cos6-i686", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-cos6-i686", "description": "(CDT) OpenJDK Runtime Environment" }, + { "name": "java-1.7.0-openjdk-cos6-x86_64", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-cos6-x86_64", "description": "(CDT) OpenJDK Runtime Environment" }, + { "name": "java-1.7.0-openjdk-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-cos7-ppc64le", "description": "(CDT) OpenJDK Runtime Environment" }, + { "name": "java-1.7.0-openjdk-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-devel-cos6-i686", "description": "(CDT) OpenJDK Development Environment" }, + { "name": "java-1.7.0-openjdk-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-devel-cos6-x86_64", "description": "(CDT) OpenJDK Development Environment" }, + { "name": "java-1.7.0-openjdk-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-devel-cos7-ppc64le", "description": "(CDT) OpenJDK Development Environment" }, + { "name": "java-1.7.0-openjdk-headless-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/java-1.7.0-openjdk-headless-cos7-ppc64le", "description": "(CDT) The OpenJDK runtime environment without audio and video support" }, + { "name": "java-1.8.0-openjdk-cos7-s390x", "uri": "https://anaconda.org/anaconda/java-1.8.0-openjdk-cos7-s390x", "description": "(CDT) OpenJDK Runtime Environment 8" }, + { "name": "java-1.8.0-openjdk-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/java-1.8.0-openjdk-devel-cos7-s390x", "description": "(CDT) OpenJDK Development Environment 8" }, + { "name": "java-1.8.0-openjdk-headless-cos7-s390x", "uri": "https://anaconda.org/anaconda/java-1.8.0-openjdk-headless-cos7-s390x", "description": "(CDT) OpenJDK Headless Runtime Environment 8" }, + { "name": "javaobj-py3", "uri": "https://anaconda.org/anaconda/javaobj-py3", "description": "Module for serializing and de-serializing Java objects." }, + { "name": "javapackages-tools-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/javapackages-tools-cos7-ppc64le", "description": "(CDT) Macros and scripts for Java packaging support" }, + { "name": "javapackages-tools-cos7-s390x", "uri": "https://anaconda.org/anaconda/javapackages-tools-cos7-s390x", "description": "(CDT) Macros and scripts for Java packaging support" }, + { "name": "jax", "uri": "https://anaconda.org/anaconda/jax", "description": "Differentiate, compile, and transform Numpy code" }, + { "name": "jax-jumpy", "uri": "https://anaconda.org/anaconda/jax-jumpy", "description": "Common backend for JAX or numpy." }, + { "name": "jaxlib", "uri": "https://anaconda.org/anaconda/jaxlib", "description": "Composable transformations of Python+NumPy programs: differentiate, vectorize, JIT to GPU/TPU, and more" }, + { "name": "jaydebeapi", "uri": "https://anaconda.org/anaconda/jaydebeapi", "description": "A Python DB-APIv2.0 compliant library for JDBC Drivers" }, + { "name": "jbigkit-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/jbigkit-libs-amzn2-aarch64", "description": "(CDT) JBIG1 lossless image compression library" }, + { "name": "jedi", "uri": "https://anaconda.org/anaconda/jedi", "description": "An autocompletion tool for Python that can be used for text editors." }, + { "name": "jeepney", "uri": "https://anaconda.org/anaconda/jeepney", "description": "Pure Python DBus interface" }, + { "name": "jellyfish", "uri": "https://anaconda.org/anaconda/jellyfish", "description": "A library for doing approximate and phonetic matching of strings" }, + { "name": "jemalloc", "uri": "https://anaconda.org/anaconda/jemalloc", "description": "general purpose malloc(3) implementation" }, + { "name": "jinja2", "uri": "https://anaconda.org/anaconda/jinja2", "description": "A very fast and expressive template engine." }, + { "name": "jinja2-time", "uri": "https://anaconda.org/anaconda/jinja2-time", "description": "Jinja2 Extension for Dates and Times" }, + { "name": "jinxed", "uri": "https://anaconda.org/anaconda/jinxed", "description": "Jinxed Terminal Library" }, + { "name": "jira", "uri": "https://anaconda.org/anaconda/jira", "description": "The easiest way to automate JIRA" }, + { "name": "jiter", "uri": "https://anaconda.org/anaconda/jiter", "description": "Fast iterable JSON parser." }, + { "name": "jmespath", "uri": "https://anaconda.org/anaconda/jmespath", "description": "Query language for JSON" }, + { "name": "joblib", "uri": "https://anaconda.org/anaconda/joblib", "description": "Lightweight pipelining: using Python functions as pipeline jobs." }, + { "name": "joserfc", "uri": "https://anaconda.org/anaconda/joserfc", "description": "Implementations of JOSE RFCs in Python" }, + { "name": "jpackage-utils-cos6-i686", "uri": "https://anaconda.org/anaconda/jpackage-utils-cos6-i686", "description": "(CDT) JPackage utilities" }, + { "name": "jpackage-utils-cos6-x86_64", "uri": "https://anaconda.org/anaconda/jpackage-utils-cos6-x86_64", "description": "(CDT) JPackage utilities" }, + { "name": "jpeg", "uri": "https://anaconda.org/anaconda/jpeg", "description": "read/write jpeg COM, EXIF, IPTC medata" }, + { "name": "jpype1", "uri": "https://anaconda.org/anaconda/jpype1", "description": "A Python to Java bridge." }, + { "name": "jq", "uri": "https://anaconda.org/anaconda/jq", "description": "A command-line JSON processor." }, + { "name": "js2py", "uri": "https://anaconda.org/anaconda/js2py", "description": "JavaScript to Python Translator & JavaScript interpreter written in 100% pure Python." }, + { "name": "jschema-to-python", "uri": "https://anaconda.org/anaconda/jschema-to-python", "description": "Generate source code for Python classes from a JSON schema." }, + { "name": "json-c", "uri": "https://anaconda.org/anaconda/json-c", "description": "A JSON implementation in C." }, + { "name": "json-merge-patch", "uri": "https://anaconda.org/anaconda/json-merge-patch", "description": "json-merge-patch library provides functions to merge json in accordance with https://tools.ietf.org/html/rfc7386" }, + { "name": "json-stream-rs-tokenizer", "uri": "https://anaconda.org/anaconda/json-stream-rs-tokenizer", "description": "A faster tokenizer for the json-stream Python library" }, + { "name": "json5", "uri": "https://anaconda.org/anaconda/json5", "description": "A Python implementation of the JSON5 data format" }, + { "name": "jsoncpp", "uri": "https://anaconda.org/anaconda/jsoncpp", "description": "A C++ library for interacting with JSON." }, + { "name": "jsondate", "uri": "https://anaconda.org/anaconda/jsondate", "description": "JSON with datetime support" }, + { "name": "jsondiff", "uri": "https://anaconda.org/anaconda/jsondiff", "description": "Diff JSON and JSON-like structures in Python" }, + { "name": "jsonlines", "uri": "https://anaconda.org/anaconda/jsonlines", "description": "Library with helpers for the jsonlines file format" }, + { "name": "jsonpatch", "uri": "https://anaconda.org/anaconda/jsonpatch", "description": "Apply JSON-Patches (RFC 6902)" }, + { "name": "jsonpath-ng", "uri": "https://anaconda.org/anaconda/jsonpath-ng", "description": "Python JSONPath Next-Generation" }, + { "name": "jsonpickle", "uri": "https://anaconda.org/anaconda/jsonpickle", "description": "Python library for serializing any arbitrary object graph into JSON" }, + { "name": "jsonpointer", "uri": "https://anaconda.org/anaconda/jsonpointer", "description": "Identify specific nodes in a JSON document (RFC 6901)" }, + { "name": "jsonschema", "uri": "https://anaconda.org/anaconda/jsonschema", "description": "An implementation of JSON Schema validation for Python" }, + { "name": "jsonschema-path", "uri": "https://anaconda.org/anaconda/jsonschema-path", "description": "JSONSchema Spec with object-oriented paths" }, + { "name": "jsonschema-specifications", "uri": "https://anaconda.org/anaconda/jsonschema-specifications", "description": "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" }, + { "name": "junit-xml", "uri": "https://anaconda.org/anaconda/junit-xml", "description": "Creates JUnit XML test result documents that can be read by tools such as Jenkins" }, + { "name": "jupyter", "uri": "https://anaconda.org/anaconda/jupyter", "description": "No Summary" }, + { "name": "jupyter-dash", "uri": "https://anaconda.org/anaconda/jupyter-dash", "description": "Dash support for the Jupyter notebook interface" }, + { "name": "jupyter-lsp", "uri": "https://anaconda.org/anaconda/jupyter-lsp", "description": "Multi-Language Server WebSocket proxy for Jupyter Server" }, + { "name": "jupyter-lsp-python", "uri": "https://anaconda.org/anaconda/jupyter-lsp-python", "description": "A metapackage for jupyter-lsp and python-lsp-server" }, + { "name": "jupyter-packaging", "uri": "https://anaconda.org/anaconda/jupyter-packaging", "description": "Jupyter Packaging Utilities" }, + { "name": "jupyter-server-mathjax", "uri": "https://anaconda.org/anaconda/jupyter-server-mathjax", "description": "MathJax resources as a Jupyter Server Extension." }, + { "name": "jupyter-server-proxy", "uri": "https://anaconda.org/anaconda/jupyter-server-proxy", "description": "Jupyter server extension to supervise and proxy web services" }, + { "name": "jupyter_bokeh", "uri": "https://anaconda.org/anaconda/jupyter_bokeh", "description": "A Jupyter extension for rendering Bokeh content." }, + { "name": "jupyter_client", "uri": "https://anaconda.org/anaconda/jupyter_client", "description": "Jupyter protocol implementation and client libraries." }, + { "name": "jupyter_console", "uri": "https://anaconda.org/anaconda/jupyter_console", "description": "Jupyter terminal console" }, + { "name": "jupyter_core", "uri": "https://anaconda.org/anaconda/jupyter_core", "description": "Core common functionality of Jupyter projects." }, + { "name": "jupyter_dashboards_bundlers", "uri": "https://anaconda.org/anaconda/jupyter_dashboards_bundlers", "description": "An add-on for Jupyter Notebook" }, + { "name": "jupyter_events", "uri": "https://anaconda.org/anaconda/jupyter_events", "description": "Jupyter Event System library" }, + { "name": "jupyter_kernel_gateway", "uri": "https://anaconda.org/anaconda/jupyter_kernel_gateway", "description": "Jupyter Kernel Gateway" }, + { "name": "jupyter_server", "uri": "https://anaconda.org/anaconda/jupyter_server", "description": "Jupyter Server" }, + { "name": "jupyter_server_fileid", "uri": "https://anaconda.org/anaconda/jupyter_server_fileid", "description": "A Jupyter Server extension providing an implementation of the File ID service." }, + { "name": "jupyter_server_terminals", "uri": "https://anaconda.org/anaconda/jupyter_server_terminals", "description": "A Jupyter Server Extension Providing Terminals." }, + { "name": "jupyter_server_ydoc", "uri": "https://anaconda.org/anaconda/jupyter_server_ydoc", "description": "A Jupyter Server Extension providing support for Y documents." }, + { "name": "jupyter_telemetry", "uri": "https://anaconda.org/anaconda/jupyter_telemetry", "description": "Telemetry for Jupyter Applications and extensions." }, + { "name": "jupyter_ydoc", "uri": "https://anaconda.org/anaconda/jupyter_ydoc", "description": "Document structures for collaborative editing using Ypy" }, + { "name": "jupyterhub", "uri": "https://anaconda.org/anaconda/jupyterhub", "description": "Multi-user server for Jupyter notebooks" }, + { "name": "jupyterhub-base", "uri": "https://anaconda.org/anaconda/jupyterhub-base", "description": "Multi-user server for Jupyter notebooks" }, + { "name": "jupyterhub-ldapauthenticator", "uri": "https://anaconda.org/anaconda/jupyterhub-ldapauthenticator", "description": "LDAP Authenticator for JupyterHub" }, + { "name": "jupyterhub-singleuser", "uri": "https://anaconda.org/anaconda/jupyterhub-singleuser", "description": "Multi-user server for Jupyter notebooks" }, + { "name": "jupyterlab", "uri": "https://anaconda.org/anaconda/jupyterlab", "description": "An extensible environment for interactive and reproducible computing, based on the Jupyter Notebook and Architecture." }, + { "name": "jupyterlab-geojson", "uri": "https://anaconda.org/anaconda/jupyterlab-geojson", "description": "GeoJSON renderer for JupyterLab" }, + { "name": "jupyterlab-git", "uri": "https://anaconda.org/anaconda/jupyterlab-git", "description": "A Git extension for JupyterLab" }, + { "name": "jupyterlab-variableinspector", "uri": "https://anaconda.org/anaconda/jupyterlab-variableinspector", "description": "Variable Inspector extension for Jupyterlab." }, + { "name": "jupyterlab_code_formatter", "uri": "https://anaconda.org/anaconda/jupyterlab_code_formatter", "description": "A JupyterLab plugin to facilitate invocation of code formatters." }, + { "name": "jupyterlab_launcher", "uri": "https://anaconda.org/anaconda/jupyterlab_launcher", "description": "A Launcher for JupyterLab based applications." }, + { "name": "jupyterlab_pygments", "uri": "https://anaconda.org/anaconda/jupyterlab_pygments", "description": "Pygments syntax coloring scheme making use of the JupyterLab CSS variables" }, + { "name": "jupyterlab_server", "uri": "https://anaconda.org/anaconda/jupyterlab_server", "description": "A set of server components for JupyterLab and JupyterLab like applications." }, + { "name": "jupyterlab_widgets", "uri": "https://anaconda.org/anaconda/jupyterlab_widgets", "description": "JupyterLab extension providing HTML widgets" }, + { "name": "jupytext", "uri": "https://anaconda.org/anaconda/jupytext", "description": "Jupyter notebooks as Markdown documents, Julia, Python or R scripts" }, + { "name": "jxrlib", "uri": "https://anaconda.org/anaconda/jxrlib", "description": "jxrlib - JPEG XR Library by Microsoft, built from Debian hosted sources." }, + { "name": "kagglehub", "uri": "https://anaconda.org/anaconda/kagglehub", "description": "Access Kaggle resources anywhere" }, + { "name": "kealib", "uri": "https://anaconda.org/anaconda/kealib", "description": "The KEA format provides an implementation of the GDAL specification within the the HDF5 file format." }, + { "name": "keras", "uri": "https://anaconda.org/anaconda/keras", "description": "Deep Learning for humans" }, + { "name": "keras-applications", "uri": "https://anaconda.org/anaconda/keras-applications", "description": "Applications module of the Keras deep learning library." }, + { "name": "keras-base", "uri": "https://anaconda.org/anaconda/keras-base", "description": "No Summary" }, + { "name": "keras-gpu", "uri": "https://anaconda.org/anaconda/keras-gpu", "description": "Deep Learning Library for Theano and TensorFlow" }, + { "name": "keras-ocr", "uri": "https://anaconda.org/anaconda/keras-ocr", "description": "A packaged and flexible version of the CRAFT text detector and Keras CRNN recognition model." }, + { "name": "keras-preprocessing", "uri": "https://anaconda.org/anaconda/keras-preprocessing", "description": "Data preprocessing and data augmentation module of the Keras deep learning library" }, + { "name": "kernel-headers-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/kernel-headers-amzn2-aarch64", "description": "(CDT) Header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers-cos6-x86_64", "uri": "https://anaconda.org/anaconda/kernel-headers-cos6-x86_64", "description": "(CDT) Header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/kernel-headers-cos7-ppc64le", "description": "(CDT) Header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers_linux-64", "uri": "https://anaconda.org/anaconda/kernel-headers_linux-64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers_linux-aarch64", "uri": "https://anaconda.org/anaconda/kernel-headers_linux-aarch64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers_linux-ppc64le", "uri": "https://anaconda.org/anaconda/kernel-headers_linux-ppc64le", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "kernel-headers_linux-s390x", "uri": "https://anaconda.org/anaconda/kernel-headers_linux-s390x", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "keyring", "uri": "https://anaconda.org/anaconda/keyring", "description": "Store and access your passwords safely" }, + { "name": "keyrings.alt", "uri": "https://anaconda.org/anaconda/keyrings.alt", "description": "Alternate keyring backend implementations for use with the keyring package." }, + { "name": "keyutils-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/keyutils-libs-amzn2-aarch64", "description": "(CDT) Key utilities library" }, + { "name": "keyutils-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/keyutils-libs-cos6-i686", "description": "(CDT) Key utilities library" }, + { "name": "keyutils-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/keyutils-libs-cos6-x86_64", "description": "(CDT) Key utilities library" }, + { "name": "keyutils-libs-cos7-s390x", "uri": "https://anaconda.org/anaconda/keyutils-libs-cos7-s390x", "description": "(CDT) Key utilities library" }, + { "name": "keyutils-libs-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/keyutils-libs-devel-amzn2-aarch64", "description": "(CDT) Development package for building Linux key management utilities" }, + { "name": "khronos-opencl-icd-loader", "uri": "https://anaconda.org/anaconda/khronos-opencl-icd-loader", "description": "A driver loader for OpenCL" }, + { "name": "kiwisolver", "uri": "https://anaconda.org/anaconda/kiwisolver", "description": "An efficient C++ implementation of the Cassowary constraint solver" }, + { "name": "kmod-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/kmod-amzn2-aarch64", "description": "(CDT) Linux kernel module management utilities" }, + { "name": "kmod-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/kmod-cos7-ppc64le", "description": "(CDT) Linux kernel module management utilities" }, + { "name": "kmod-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/kmod-libs-amzn2-aarch64", "description": "(CDT) Libraries to handle kernel module loading and unloading" }, + { "name": "kmodes", "uri": "https://anaconda.org/anaconda/kmodes", "description": "Python implementations of the k-modes and k-prototypes clustering algorithms for clustering categorical data." }, + { "name": "knit", "uri": "https://anaconda.org/anaconda/knit", "description": "Python interface YARN" }, + { "name": "kombu", "uri": "https://anaconda.org/anaconda/kombu", "description": "Messaging library for Python" }, + { "name": "korean_lunar_calendar", "uri": "https://anaconda.org/anaconda/korean_lunar_calendar", "description": "Korean Lunar Calendar" }, + { "name": "krb5", "uri": "https://anaconda.org/anaconda/krb5", "description": "A network authentication protocol." }, + { "name": "krb5-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/krb5-devel-amzn2-aarch64", "description": "(CDT) Development files needed to compile Kerberos 5 programs" }, + { "name": "krb5-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/krb5-libs-amzn2-aarch64", "description": "(CDT) The non-admin shared libraries used by Kerberos 5" }, + { "name": "krb5-libs-cos6-i686", "uri": "https://anaconda.org/anaconda/krb5-libs-cos6-i686", "description": "(CDT) The non-admin shared libraries used by Kerberos 5" }, + { "name": "krb5-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/krb5-libs-cos6-x86_64", "description": "(CDT) The non-admin shared libraries used by Kerberos 5" }, + { "name": "krb5-libs-cos7-s390x", "uri": "https://anaconda.org/anaconda/krb5-libs-cos7-s390x", "description": "(CDT) The non-admin shared libraries used by Kerberos 5" }, + { "name": "krb5-static", "uri": "https://anaconda.org/anaconda/krb5-static", "description": "A network authentication protocol." }, + { "name": "kt-legacy", "uri": "https://anaconda.org/anaconda/kt-legacy", "description": "Legacy import names for Keras Tuner" }, + { "name": "lame", "uri": "https://anaconda.org/anaconda/lame", "description": "High quality MPEG Audio Layer III (MP3) encoder" }, + { "name": "langchain", "uri": "https://anaconda.org/anaconda/langchain", "description": "Building applications with LLMs through composability" }, + { "name": "langchain-community", "uri": "https://anaconda.org/anaconda/langchain-community", "description": "Community contributed LangChain integrations." }, + { "name": "langchain-core", "uri": "https://anaconda.org/anaconda/langchain-core", "description": "Core APIs for LangChain, the LLM framework for buildilng applications through composability" }, + { "name": "langchain-text-splitters", "uri": "https://anaconda.org/anaconda/langchain-text-splitters", "description": "LangChain text splitting utilities" }, + { "name": "langcodes", "uri": "https://anaconda.org/anaconda/langcodes", "description": "Labels and compares human languages in a standardized way" }, + { "name": "langsmith", "uri": "https://anaconda.org/anaconda/langsmith", "description": "Client library to connect to the LangSmith language model tracing and evaluation API." }, + { "name": "lapack", "uri": "https://anaconda.org/anaconda/lapack", "description": "Linear Algebra PACKage" }, + { "name": "lark", "uri": "https://anaconda.org/anaconda/lark", "description": "a modern parsing library" }, + { "name": "lazrs-python", "uri": "https://anaconda.org/anaconda/lazrs-python", "description": "Python bindings for laz-rs" }, + { "name": "lazy-object-proxy", "uri": "https://anaconda.org/anaconda/lazy-object-proxy", "description": "A fast and thorough lazy object proxy" }, + { "name": "lazy_loader", "uri": "https://anaconda.org/anaconda/lazy_loader", "description": "Easily load subpackages and functions on demand" }, + { "name": "lcms2", "uri": "https://anaconda.org/anaconda/lcms2", "description": "Open Source Color Management Engine" }, + { "name": "ld64", "uri": "https://anaconda.org/anaconda/ld64", "description": "Darwin Mach-O native linker" }, + { "name": "ld64_linux-64", "uri": "https://anaconda.org/anaconda/ld64_linux-64", "description": "Darwin Mach-O cross linker" }, + { "name": "ld64_linux-aarch64", "uri": "https://anaconda.org/anaconda/ld64_linux-aarch64", "description": "Darwin Mach-O cross linker" }, + { "name": "ld64_linux-ppc64le", "uri": "https://anaconda.org/anaconda/ld64_linux-ppc64le", "description": "Darwin Mach-O cross linker" }, + { "name": "ld64_osx-64", "uri": "https://anaconda.org/anaconda/ld64_osx-64", "description": "Darwin Mach-O cross linker" }, + { "name": "ld64_osx-arm64", "uri": "https://anaconda.org/anaconda/ld64_osx-arm64", "description": "Darwin Mach-O cross linker" }, + { "name": "ld_impl_linux-64", "uri": "https://anaconda.org/anaconda/ld_impl_linux-64", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "ld_impl_linux-aarch64", "uri": "https://anaconda.org/anaconda/ld_impl_linux-aarch64", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "ld_impl_linux-ppc64le", "uri": "https://anaconda.org/anaconda/ld_impl_linux-ppc64le", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "ld_impl_linux-s390x", "uri": "https://anaconda.org/anaconda/ld_impl_linux-s390x", "description": "A set of programming tools for creating and managing binary programs, object files,\nlibraries, profile data, and assembly source code." }, + { "name": "ldid", "uri": "https://anaconda.org/anaconda/ldid", "description": "pseudo-codesign Mach-O files" }, + { "name": "leather", "uri": "https://anaconda.org/anaconda/leather", "description": "Python charting for 80% of humans." }, + { "name": "leb128", "uri": "https://anaconda.org/anaconda/leb128", "description": "LEB128(Little Endian Base 128)" }, + { "name": "leptonica", "uri": "https://anaconda.org/anaconda/leptonica", "description": "Useful for image processing and image analysis applications" }, + { "name": "lerc", "uri": "https://anaconda.org/anaconda/lerc", "description": "LERC - Limited Error Raster Compression" }, + { "name": "liac-arff", "uri": "https://anaconda.org/anaconda/liac-arff", "description": "A module for read and write ARFF files in Python." }, + { "name": "libabseil", "uri": "https://anaconda.org/anaconda/libabseil", "description": "Abseil Common Libraries (C++)" }, + { "name": "libabseil-tests", "uri": "https://anaconda.org/anaconda/libabseil-tests", "description": "Abseil Common Libraries (C++)" }, + { "name": "libacl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libacl-amzn2-aarch64", "description": "(CDT) Dynamic library for access control list support" }, + { "name": "libaec", "uri": "https://anaconda.org/anaconda/libaec", "description": "Adaptive Entropy Coding library" }, + { "name": "libaio", "uri": "https://anaconda.org/anaconda/libaio", "description": "Provides the Linux-native API for async I/O" }, + { "name": "libansicon", "uri": "https://anaconda.org/anaconda/libansicon", "description": "ansi console for windows" }, + { "name": "libapr", "uri": "https://anaconda.org/anaconda/libapr", "description": "Maintains a consistent API with predictable behaviour" }, + { "name": "libapriconv", "uri": "https://anaconda.org/anaconda/libapriconv", "description": "Maintains a consistent API with predictable behaviour" }, + { "name": "libaprutil", "uri": "https://anaconda.org/anaconda/libaprutil", "description": "Maintains a consistent API with predictable behaviour" }, + { "name": "libarchive", "uri": "https://anaconda.org/anaconda/libarchive", "description": "Multi-format archive and compression library" }, + { "name": "libart_lgpl-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libart_lgpl-cos6-x86_64", "description": "(CDT) Library of graphics routines used by libgnomecanvas" }, + { "name": "libattr-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libattr-amzn2-aarch64", "description": "(CDT) Dynamic library for extended attribute support" }, + { "name": "libavif", "uri": "https://anaconda.org/anaconda/libavif", "description": "A friendly, portable C implementation of the AV1 Image File Format" }, + { "name": "libblas", "uri": "https://anaconda.org/anaconda/libblas", "description": "Linear Algebra PACKage" }, + { "name": "libblkid-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libblkid-amzn2-aarch64", "description": "(CDT) Block device ID library" }, + { "name": "libblkid-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libblkid-cos6-x86_64", "description": "(CDT) Block device ID library" }, + { "name": "libbonobo-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libbonobo-cos6-x86_64", "description": "(CDT) Bonobo component system" }, + { "name": "libbonobo-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libbonobo-devel-cos6-x86_64", "description": "(CDT) Libraries and headers for libbonobo" }, + { "name": "libboost", "uri": "https://anaconda.org/anaconda/libboost", "description": "Free peer-reviewed portable C++ source libraries." }, + { "name": "libbrotlicommon", "uri": "https://anaconda.org/anaconda/libbrotlicommon", "description": "Brotli compression format" }, + { "name": "libbrotlidec", "uri": "https://anaconda.org/anaconda/libbrotlidec", "description": "Brotli compression format" }, + { "name": "libbrotlienc", "uri": "https://anaconda.org/anaconda/libbrotlienc", "description": "Brotli compression format" }, + { "name": "libcap-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcap-amzn2-aarch64", "description": "(CDT) Library for getting and setting POSIX.1e capabilities" }, + { "name": "libcap-ng-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcap-ng-amzn2-aarch64", "description": "(CDT) An alternate posix capabilities library" }, + { "name": "libcblas", "uri": "https://anaconda.org/anaconda/libcblas", "description": "Linear Algebra PACKage" }, + { "name": "libclang", "uri": "https://anaconda.org/anaconda/libclang", "description": "Development headers and libraries for Clang" }, + { "name": "libclang-cpp", "uri": "https://anaconda.org/anaconda/libclang-cpp", "description": "Development headers and libraries for Clang" }, + { "name": "libclang-cpp10", "uri": "https://anaconda.org/anaconda/libclang-cpp10", "description": "Development headers and libraries for Clang" }, + { "name": "libclang-cpp12", "uri": "https://anaconda.org/anaconda/libclang-cpp12", "description": "Development headers and libraries for Clang" }, + { "name": "libclang-cpp14", "uri": "https://anaconda.org/anaconda/libclang-cpp14", "description": "Development headers and libraries for Clang" }, + { "name": "libclang13", "uri": "https://anaconda.org/anaconda/libclang13", "description": "Development headers and libraries for Clang" }, + { "name": "libcom_err-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcom_err-amzn2-aarch64", "description": "(CDT) Common error description library" }, + { "name": "libcom_err-cos6-i686", "uri": "https://anaconda.org/anaconda/libcom_err-cos6-i686", "description": "(CDT) Common error description library" }, + { "name": "libcom_err-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libcom_err-cos6-x86_64", "description": "(CDT) Common error description library" }, + { "name": "libcom_err-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcom_err-devel-amzn2-aarch64", "description": "(CDT) Common error description library" }, + { "name": "libcrc32c", "uri": "https://anaconda.org/anaconda/libcrc32c", "description": "CRC32C implementation with support for CPU-specific acceleration instructions" }, + { "name": "libcroco-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcroco-amzn2-aarch64", "description": "(CDT) A CSS2 parsing library" }, + { "name": "libcrypt-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcrypt-amzn2-aarch64", "description": "(CDT) Password hashing library (non-NSS version)" }, + { "name": "libcryptominisat", "uri": "https://anaconda.org/anaconda/libcryptominisat", "description": "An advanced SAT Solver https://www.msoos.org" }, + { "name": "libcst", "uri": "https://anaconda.org/anaconda/libcst", "description": "A concrete syntax tree with AST-like properties for Python 3.5, 3.6, 3.7 and 3.8 programs." }, + { "name": "libcublas", "uri": "https://anaconda.org/anaconda/libcublas", "description": "An implementation of BLAS (Basic Linear Algebra Subprograms) on top of the NVIDIA CUDA runtime." }, + { "name": "libcublas-dev", "uri": "https://anaconda.org/anaconda/libcublas-dev", "description": "An implementation of BLAS (Basic Linear Algebra Subprograms) on top of the NVIDIA CUDA runtime." }, + { "name": "libcublas-static", "uri": "https://anaconda.org/anaconda/libcublas-static", "description": "An implementation of BLAS (Basic Linear Algebra Subprograms) on top of the NVIDIA CUDA runtime." }, + { "name": "libcufft", "uri": "https://anaconda.org/anaconda/libcufft", "description": "cuFFT native runtime libraries" }, + { "name": "libcufft-dev", "uri": "https://anaconda.org/anaconda/libcufft-dev", "description": "cuFFT native runtime libraries" }, + { "name": "libcufft-static", "uri": "https://anaconda.org/anaconda/libcufft-static", "description": "cuFFT native runtime libraries" }, + { "name": "libcufile", "uri": "https://anaconda.org/anaconda/libcufile", "description": "Library for NVIDIA GPUDirect Storage" }, + { "name": "libcufile-dev", "uri": "https://anaconda.org/anaconda/libcufile-dev", "description": "Library for NVIDIA GPUDirect Storage" }, + { "name": "libcufile-static", "uri": "https://anaconda.org/anaconda/libcufile-static", "description": "Library for NVIDIA GPUDirect Storage" }, + { "name": "libcurand", "uri": "https://anaconda.org/anaconda/libcurand", "description": "cuRAND native runtime libraries" }, + { "name": "libcurand-dev", "uri": "https://anaconda.org/anaconda/libcurand-dev", "description": "cuRAND native runtime libraries" }, + { "name": "libcurand-static", "uri": "https://anaconda.org/anaconda/libcurand-static", "description": "cuRAND native runtime libraries" }, + { "name": "libcurl", "uri": "https://anaconda.org/anaconda/libcurl", "description": "tool and library for transferring data with URL syntax" }, + { "name": "libcurl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libcurl-amzn2-aarch64", "description": "(CDT) A library for getting files from web servers" }, + { "name": "libcurl-static", "uri": "https://anaconda.org/anaconda/libcurl-static", "description": "tool and library for transferring data with URL syntax" }, + { "name": "libcusolver", "uri": "https://anaconda.org/anaconda/libcusolver", "description": "CUDA Linear Solver Library" }, + { "name": "libcusolver-dev", "uri": "https://anaconda.org/anaconda/libcusolver-dev", "description": "CUDA Linear Solver Library" }, + { "name": "libcusolver-static", "uri": "https://anaconda.org/anaconda/libcusolver-static", "description": "CUDA Linear Solver Library" }, + { "name": "libcusparse", "uri": "https://anaconda.org/anaconda/libcusparse", "description": "CUDA Sparse Matrix Library" }, + { "name": "libcusparse-dev", "uri": "https://anaconda.org/anaconda/libcusparse-dev", "description": "CUDA Sparse Matrix Library" }, + { "name": "libcusparse-static", "uri": "https://anaconda.org/anaconda/libcusparse-static", "description": "CUDA Sparse Matrix Library" }, + { "name": "libcxxabi", "uri": "https://anaconda.org/anaconda/libcxxabi", "description": "LLVM C++ standard library" }, + { "name": "libdap4", "uri": "https://anaconda.org/anaconda/libdap4", "description": "A C++ SDK which contains an implementation of both DAP2 and DAP4." }, + { "name": "libdate", "uri": "https://anaconda.org/anaconda/libdate", "description": "A date and time library based on the C++11/14/17 header" }, + { "name": "libdb", "uri": "https://anaconda.org/anaconda/libdb", "description": "The Berkeley DB embedded database system." }, + { "name": "libdb-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libdb-amzn2-aarch64", "description": "(CDT) The Berkeley DB database library for C" }, + { "name": "libdeflate", "uri": "https://anaconda.org/anaconda/libdeflate", "description": "libdeflate is a library for fast, whole-buffer DEFLATE-based compression and decompression." }, + { "name": "libdrm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libdrm-amzn2-aarch64", "description": "(CDT) Direct Rendering Manager runtime library" }, + { "name": "libdrm-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libdrm-cos7-ppc64le", "description": "(CDT) Direct Rendering Manager runtime library" }, + { "name": "libdrm-cos7-s390x", "uri": "https://anaconda.org/anaconda/libdrm-cos7-s390x", "description": "(CDT) Direct Rendering Manager runtime library" }, + { "name": "libdrm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libdrm-devel-amzn2-aarch64", "description": "(CDT) Direct Rendering Manager development package" }, + { "name": "libdrm-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libdrm-devel-cos6-i686", "description": "(CDT) Direct Rendering Manager development package" }, + { "name": "libdrm-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libdrm-devel-cos7-ppc64le", "description": "(CDT) Direct Rendering Manager development package" }, + { "name": "libedit", "uri": "https://anaconda.org/anaconda/libedit", "description": "Editline Library (libedit)" }, + { "name": "libev", "uri": "https://anaconda.org/anaconda/libev", "description": "A full-featured and high-performance event loop that is loosely modeled after libevent, but without its limitations and bugs." }, + { "name": "libev-libevent", "uri": "https://anaconda.org/anaconda/libev-libevent", "description": "A full-featured and high-performance event loop that is loosely modeled after libevent, but without its limitations and bugs." }, + { "name": "libev-static", "uri": "https://anaconda.org/anaconda/libev-static", "description": "A full-featured and high-performance event loop that is loosely modeled after libevent, but without its limitations and bugs." }, + { "name": "libffi", "uri": "https://anaconda.org/anaconda/libffi", "description": "A Portable Foreign Function Interface Library" }, + { "name": "libffi-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libffi-amzn2-aarch64", "description": "(CDT) A portable foreign function interface library" }, + { "name": "libgcc-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libgcc-amzn2-aarch64", "description": "(CDT) GCC version 7 shared support library" }, + { "name": "libgcc-devel_linux-64", "uri": "https://anaconda.org/anaconda/libgcc-devel_linux-64", "description": "The GNU C development libraries and object files" }, + { "name": "libgcc-devel_linux-aarch64", "uri": "https://anaconda.org/anaconda/libgcc-devel_linux-aarch64", "description": "The GNU C development libraries and object files" }, + { "name": "libgcc-devel_linux-ppc64le", "uri": "https://anaconda.org/anaconda/libgcc-devel_linux-ppc64le", "description": "The GNU C development libraries and object files" }, + { "name": "libgcc-devel_linux-s390x", "uri": "https://anaconda.org/anaconda/libgcc-devel_linux-s390x", "description": "The GNU C development libraries and object files" }, + { "name": "libgcc-ng", "uri": "https://anaconda.org/anaconda/libgcc-ng", "description": "The GCC low-level runtime library" }, + { "name": "libgcj-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libgcj-cos6-x86_64", "description": "(CDT) Java runtime library for gcc" }, + { "name": "libgcrypt", "uri": "https://anaconda.org/anaconda/libgcrypt", "description": "a general purpose cryptographic library originally based on code from GnuPG." }, + { "name": "libgcrypt-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libgcrypt-amzn2-aarch64", "description": "(CDT) A general-purpose cryptography library" }, + { "name": "libgcrypt-cos6-i686", "uri": "https://anaconda.org/anaconda/libgcrypt-cos6-i686", "description": "(CDT) A general-purpose cryptography library" }, + { "name": "libgcrypt-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libgcrypt-cos6-x86_64", "description": "(CDT) A general-purpose cryptography library" }, + { "name": "libgcrypt-cos7-s390x", "uri": "https://anaconda.org/anaconda/libgcrypt-cos7-s390x", "description": "(CDT) A general-purpose cryptography library" }, + { "name": "libgd", "uri": "https://anaconda.org/anaconda/libgd", "description": "Library for the dynamic creation of images." }, + { "name": "libgdal", "uri": "https://anaconda.org/anaconda/libgdal", "description": "The Geospatial Data Abstraction Library (GDAL)" }, + { "name": "libgfortran-devel_osx-64", "uri": "https://anaconda.org/anaconda/libgfortran-devel_osx-64", "description": "Fortran compiler and libraries from the GNU Compiler Collection" }, + { "name": "libgfortran-devel_osx-arm64", "uri": "https://anaconda.org/anaconda/libgfortran-devel_osx-arm64", "description": "Fortran compiler and libraries from the GNU Compiler Collection" }, + { "name": "libgfortran-ng", "uri": "https://anaconda.org/anaconda/libgfortran-ng", "description": "The GNU Fortran Runtime Library" }, + { "name": "libgfortran4", "uri": "https://anaconda.org/anaconda/libgfortran4", "description": "The GNU Fortran Runtime Library" }, + { "name": "libgfortran5", "uri": "https://anaconda.org/anaconda/libgfortran5", "description": "Fortran compiler and libraries from the GNU Compiler Collection" }, + { "name": "libgit2", "uri": "https://anaconda.org/anaconda/libgit2", "description": "libgit2 is a portable, pure C implementation of the Git core methods provided as a re-entrant linkable library with a solid API, allowing you to write native speed custom Git applications in any language which supports C bindings." }, + { "name": "libglib", "uri": "https://anaconda.org/anaconda/libglib", "description": "Provides core application building blocks for libraries and applications written in C." }, + { "name": "libglu", "uri": "https://anaconda.org/anaconda/libglu", "description": "Mesa OpenGL utility library (GLU)" }, + { "name": "libglvnd-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-amzn2-aarch64", "description": "(CDT) The GL Vendor-Neutral Dispatch library" }, + { "name": "libglvnd-core-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-core-devel-amzn2-aarch64", "description": "(CDT) Core development files for libglvnd" }, + { "name": "libglvnd-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libglvnd-cos7-ppc64le", "description": "(CDT) The GL Vendor-Neutral Dispatch library" }, + { "name": "libglvnd-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-devel-amzn2-aarch64", "description": "(CDT) Development files for libglvnd" }, + { "name": "libglvnd-egl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-egl-amzn2-aarch64", "description": "(CDT) EGL support for libglvnd" }, + { "name": "libglvnd-gles-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-gles-amzn2-aarch64", "description": "(CDT) GLES support for libglvnd" }, + { "name": "libglvnd-glx-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-glx-amzn2-aarch64", "description": "(CDT) GLX support for libglvnd" }, + { "name": "libglvnd-glx-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libglvnd-glx-cos7-ppc64le", "description": "(CDT) GLX support for libglvnd" }, + { "name": "libglvnd-opengl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libglvnd-opengl-amzn2-aarch64", "description": "(CDT) OpenGL support for libglvnd" }, + { "name": "libgomp", "uri": "https://anaconda.org/anaconda/libgomp", "description": "The GCC OpenMP implementation." }, + { "name": "libgomp-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libgomp-amzn2-aarch64", "description": "(CDT) GCC OpenMP v4.5 shared support library" }, + { "name": "libgpg-error", "uri": "https://anaconda.org/anaconda/libgpg-error", "description": "a small library that originally defined common error values for all GnuPG components" }, + { "name": "libgpg-error-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libgpg-error-amzn2-aarch64", "description": "(CDT) Library for error values used by GnuPG components" }, + { "name": "libgpg-error-cos6-i686", "uri": "https://anaconda.org/anaconda/libgpg-error-cos6-i686", "description": "(CDT) Library for error values used by GnuPG components" }, + { "name": "libgpg-error-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libgpg-error-cos6-x86_64", "description": "(CDT) Library for error values used by GnuPG components" }, + { "name": "libgpuarray", "uri": "https://anaconda.org/anaconda/libgpuarray", "description": "Library to manipulate arrays on GPU" }, + { "name": "libgrpc", "uri": "https://anaconda.org/anaconda/libgrpc", "description": "gRPC - A high-performance, open-source universal RPC framework" }, + { "name": "libgsasl", "uri": "https://anaconda.org/anaconda/libgsasl", "description": "Implementation of the Simple Authentication and Security Layer framework" }, + { "name": "libgsf", "uri": "https://anaconda.org/anaconda/libgsf", "description": "The G Structured File Library" }, + { "name": "libhdfs3", "uri": "https://anaconda.org/anaconda/libhdfs3", "description": "A Native C/C++ HDFS Client" }, + { "name": "libice-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libice-amzn2-aarch64", "description": "(CDT) X.Org X11 ICE runtime library" }, + { "name": "libice-cos6-i686", "uri": "https://anaconda.org/anaconda/libice-cos6-i686", "description": "(CDT) X.Org X11 ICE runtime library" }, + { "name": "libice-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libice-cos6-x86_64", "description": "(CDT) X.Org X11 ICE runtime library" }, + { "name": "libice-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libice-cos7-ppc64le", "description": "(CDT) X.Org X11 ICE runtime library" }, + { "name": "libice-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libice-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 ICE development package" }, + { "name": "libice-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libice-devel-cos6-i686", "description": "(CDT) X.Org X11 ICE development package" }, + { "name": "libice-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libice-devel-cos6-x86_64", "description": "(CDT) X.Org X11 ICE development package" }, + { "name": "libice-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libice-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 ICE development package" }, + { "name": "libice-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libice-devel-cos7-s390x", "description": "(CDT) X.Org X11 ICE development package" }, + { "name": "libiconv", "uri": "https://anaconda.org/anaconda/libiconv", "description": "Provides iconv for systems which don't have one (or that cannot convert from/to Unicode.)" }, + { "name": "libidl-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libidl-cos6-x86_64", "description": "(CDT) Library for parsing IDL (Interface Definition Language)" }, + { "name": "libidl-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libidl-devel-cos6-x86_64", "description": "(CDT) Development libraries and header files for libIDL" }, + { "name": "libidn11", "uri": "https://anaconda.org/anaconda/libidn11", "description": "Library for internationalized domain name support" }, + { "name": "libidn2", "uri": "https://anaconda.org/anaconda/libidn2", "description": "Library for internationalized domain names (IDNA2008) support" }, + { "name": "libjpeg-turbo", "uri": "https://anaconda.org/anaconda/libjpeg-turbo", "description": "IJG JPEG compliant runtime library with SIMD and other optimizations" }, + { "name": "libjpeg-turbo-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libjpeg-turbo-amzn2-aarch64", "description": "(CDT) A MMX/SSE2 accelerated library for manipulating JPEG image files" }, + { "name": "libjpeg-turbo-cos6-i686", "uri": "https://anaconda.org/anaconda/libjpeg-turbo-cos6-i686", "description": "(CDT) A MMX/SSE2 accelerated library for manipulating JPEG image files" }, + { "name": "libjpeg-turbo-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libjpeg-turbo-cos6-x86_64", "description": "(CDT) A MMX/SSE2 accelerated library for manipulating JPEG image files" }, + { "name": "libjpeg-turbo-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libjpeg-turbo-cos7-ppc64le", "description": "(CDT) A MMX/SSE2 accelerated library for manipulating JPEG image files" }, + { "name": "libkadm5-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libkadm5-amzn2-aarch64", "description": "(CDT) Kerberos 5 Administrative libraries" }, + { "name": "libkml", "uri": "https://anaconda.org/anaconda/libkml", "description": "Reference implementation of OGC KML 2.2" }, + { "name": "liblapack", "uri": "https://anaconda.org/anaconda/liblapack", "description": "Linear Algebra PACKage" }, + { "name": "liblapacke", "uri": "https://anaconda.org/anaconda/liblapacke", "description": "Linear Algebra PACKage" }, + { "name": "liblief", "uri": "https://anaconda.org/anaconda/liblief", "description": "A cross platform library to parse, modify and abstract ELF, PE and MachO formats." }, + { "name": "libllvm10", "uri": "https://anaconda.org/anaconda/libllvm10", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm11", "uri": "https://anaconda.org/anaconda/libllvm11", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm12", "uri": "https://anaconda.org/anaconda/libllvm12", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm14", "uri": "https://anaconda.org/anaconda/libllvm14", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm15", "uri": "https://anaconda.org/anaconda/libllvm15", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm17", "uri": "https://anaconda.org/anaconda/libllvm17", "description": "Development headers and libraries for LLVM" }, + { "name": "libllvm9", "uri": "https://anaconda.org/anaconda/libllvm9", "description": "Development headers and libraries for LLVM" }, + { "name": "libmagic", "uri": "https://anaconda.org/anaconda/libmagic", "description": "Implementation of the file(1) command" }, + { "name": "libmamba", "uri": "https://anaconda.org/anaconda/libmamba", "description": "A fast drop-in alternative to conda, using libsolv for dependency resolution" }, + { "name": "libmambapy", "uri": "https://anaconda.org/anaconda/libmambapy", "description": "A fast drop-in alternative to conda, using libsolv for dependency resolution" }, + { "name": "libmicrohttpd", "uri": "https://anaconda.org/anaconda/libmicrohttpd", "description": "Light HTTP/1.1 server library" }, + { "name": "libmklml", "uri": "https://anaconda.org/anaconda/libmklml", "description": "No Summary" }, + { "name": "libml_dtypes-headers", "uri": "https://anaconda.org/anaconda/libml_dtypes-headers", "description": "A stand-alone implementation of several NumPy dtype extensions used in machine learning." }, + { "name": "libmlir", "uri": "https://anaconda.org/anaconda/libmlir", "description": "Multi-Level IR Compiler Framework" }, + { "name": "libmlir12", "uri": "https://anaconda.org/anaconda/libmlir12", "description": "Multi-Level IR Compiler Framework" }, + { "name": "libmlir14", "uri": "https://anaconda.org/anaconda/libmlir14", "description": "Multi-Level IR Compiler Framework" }, + { "name": "libmlir17", "uri": "https://anaconda.org/anaconda/libmlir17", "description": "Multi-Level IR Compiler Framework" }, + { "name": "libmount-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libmount-amzn2-aarch64", "description": "(CDT) Device mounting library" }, + { "name": "libmpdec", "uri": "https://anaconda.org/anaconda/libmpdec", "description": "A package for correctly-rounded arbitrary precision decimal floating point arithmetic" }, + { "name": "libmpdec-devel", "uri": "https://anaconda.org/anaconda/libmpdec-devel", "description": "A package for correctly-rounded arbitrary precision decimal floating point arithmetic" }, + { "name": "libmpdecxx", "uri": "https://anaconda.org/anaconda/libmpdecxx", "description": "A package for correctly-rounded arbitrary precision decimal floating point arithmetic" }, + { "name": "libmpdecxx-devel", "uri": "https://anaconda.org/anaconda/libmpdecxx-devel", "description": "A package for correctly-rounded arbitrary precision decimal floating point arithmetic" }, + { "name": "libmxnet", "uri": "https://anaconda.org/anaconda/libmxnet", "description": "MXNet is a deep learning framework designed for both efficiency and flexibility" }, + { "name": "libnetcdf", "uri": "https://anaconda.org/anaconda/libnetcdf", "description": "Libraries and data formats that support array-oriented scientific data." }, + { "name": "libnghttp2", "uri": "https://anaconda.org/anaconda/libnghttp2", "description": "This is an implementation of Hypertext Transfer Protocol version 2." }, + { "name": "libnghttp2-static", "uri": "https://anaconda.org/anaconda/libnghttp2-static", "description": "This is an implementation of Hypertext Transfer Protocol version 2." }, + { "name": "libnpp", "uri": "https://anaconda.org/anaconda/libnpp", "description": "NPP native runtime libraries" }, + { "name": "libnpp-dev", "uri": "https://anaconda.org/anaconda/libnpp-dev", "description": "NPP native runtime libraries" }, + { "name": "libnpp-static", "uri": "https://anaconda.org/anaconda/libnpp-static", "description": "NPP native runtime libraries" }, + { "name": "libnsl", "uri": "https://anaconda.org/anaconda/libnsl", "description": "Public client interface library for NIS(YP)" }, + { "name": "libnvfatbin", "uri": "https://anaconda.org/anaconda/libnvfatbin", "description": "NVIDIA compiler library for fatbin interaction" }, + { "name": "libnvfatbin-dev", "uri": "https://anaconda.org/anaconda/libnvfatbin-dev", "description": "NVIDIA compiler library for fatbin interaction" }, + { "name": "libnvfatbin-static", "uri": "https://anaconda.org/anaconda/libnvfatbin-static", "description": "NVIDIA compiler library for fatbin interaction" }, + { "name": "libnvjitlink", "uri": "https://anaconda.org/anaconda/libnvjitlink", "description": "CUDA nvJitLink library" }, + { "name": "libnvjitlink-dev", "uri": "https://anaconda.org/anaconda/libnvjitlink-dev", "description": "CUDA nvJitLink library" }, + { "name": "libnvjitlink-static", "uri": "https://anaconda.org/anaconda/libnvjitlink-static", "description": "CUDA nvJitLink library" }, + { "name": "libnvjpeg", "uri": "https://anaconda.org/anaconda/libnvjpeg", "description": "nvJPEG native runtime libraries" }, + { "name": "libnvjpeg-dev", "uri": "https://anaconda.org/anaconda/libnvjpeg-dev", "description": "nvJPEG native runtime libraries" }, + { "name": "libnvjpeg-static", "uri": "https://anaconda.org/anaconda/libnvjpeg-static", "description": "nvJPEG native runtime libraries" }, + { "name": "libogg", "uri": "https://anaconda.org/anaconda/libogg", "description": "OGG media container" }, + { "name": "libopenblas", "uri": "https://anaconda.org/anaconda/libopenblas", "description": "An Optimized BLAS library" }, + { "name": "libopenblas-static", "uri": "https://anaconda.org/anaconda/libopenblas-static", "description": "OpenBLAS static libraries." }, + { "name": "libopencv", "uri": "https://anaconda.org/anaconda/libopencv", "description": "Computer vision and machine learning software library." }, + { "name": "libopenssl-static", "uri": "https://anaconda.org/anaconda/libopenssl-static", "description": "OpenSSL is an open-source implementation of the SSL and TLS protocols" }, + { "name": "libopus", "uri": "https://anaconda.org/anaconda/libopus", "description": "Opus Interactive Audio Codec" }, + { "name": "libosqp", "uri": "https://anaconda.org/anaconda/libosqp", "description": "The Operator Splitting QP Solver." }, + { "name": "libpcap", "uri": "https://anaconda.org/anaconda/libpcap", "description": "the LIBpcap interface to various kernel packet capture mechanism" }, + { "name": "libpng-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libpng-amzn2-aarch64", "description": "(CDT) A library of functions for manipulating PNG image format files" }, + { "name": "libpng-cos6-i686", "uri": "https://anaconda.org/anaconda/libpng-cos6-i686", "description": "(CDT) A library of functions for manipulating PNG image format files" }, + { "name": "libpng-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libpng-cos6-x86_64", "description": "(CDT) A library of functions for manipulating PNG image format files" }, + { "name": "libpng-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libpng-devel-cos6-i686", "description": "(CDT) Development tools for programs to manipulate PNG image format files" }, + { "name": "libpng-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libpng-devel-cos6-x86_64", "description": "(CDT) Development tools for programs to manipulate PNG image format files" }, + { "name": "libpng-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libpng-devel-cos7-s390x", "description": "(CDT) Development tools for programs to manipulate PNG image format files" }, + { "name": "libpq", "uri": "https://anaconda.org/anaconda/libpq", "description": "The postgres runtime libraries and utilities (not the server itself)" }, + { "name": "libprotobuf", "uri": "https://anaconda.org/anaconda/libprotobuf", "description": "Protocol Buffers - Google's data interchange format. C++ Libraries and protoc, the protobuf compiler." }, + { "name": "libprotobuf-python-headers", "uri": "https://anaconda.org/anaconda/libprotobuf-python-headers", "description": "Protocol Buffers - Google's data interchange format. C++ Libraries and protoc, the protobuf compiler." }, + { "name": "libprotobuf-static", "uri": "https://anaconda.org/anaconda/libprotobuf-static", "description": "Protocol Buffers - Google's data interchange format. C++ Libraries and protoc, the protobuf compiler." }, + { "name": "libpwquality-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libpwquality-amzn2-aarch64", "description": "(CDT) A library for password generation and password quality checking" }, + { "name": "libpysal", "uri": "https://anaconda.org/anaconda/libpysal", "description": "Core components of PySAL A library of spatial analysis functions" }, + { "name": "libpython", "uri": "https://anaconda.org/anaconda/libpython", "description": "A mingw-w64 import library for python??.dll (on Windows)" }, + { "name": "libpython-static", "uri": "https://anaconda.org/anaconda/libpython-static", "description": "General purpose programming language" }, + { "name": "libqdldl", "uri": "https://anaconda.org/anaconda/libqdldl", "description": "A free LDL factorisation routine for quasi-definite linear systems." }, + { "name": "librdkafka", "uri": "https://anaconda.org/anaconda/librdkafka", "description": "The Apache Kafka C/C++ client library" }, + { "name": "librsvg", "uri": "https://anaconda.org/anaconda/librsvg", "description": "librsvg is a library to render SVG files using cairo." }, + { "name": "libselinux-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libselinux-amzn2-aarch64", "description": "(CDT) SELinux library and simple utilities" }, + { "name": "libselinux-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libselinux-cos6-x86_64", "description": "(CDT) SELinux library and simple utilities" }, + { "name": "libselinux-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libselinux-cos7-ppc64le", "description": "(CDT) SELinux library and simple utilities" }, + { "name": "libselinux-cos7-s390x", "uri": "https://anaconda.org/anaconda/libselinux-cos7-s390x", "description": "(CDT) SELinux library and simple utilities" }, + { "name": "libselinux-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libselinux-devel-amzn2-aarch64", "description": "(CDT) Header files and libraries used to build SELinux" }, + { "name": "libselinux-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libselinux-devel-cos6-x86_64", "description": "(CDT) Header files and libraries used to build SELinux" }, + { "name": "libselinux-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libselinux-devel-cos7-ppc64le", "description": "(CDT) Header files and libraries used to build SELinux" }, + { "name": "libselinux-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libselinux-devel-cos7-s390x", "description": "(CDT) Header files and libraries used to build SELinux" }, + { "name": "libsemanage-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsemanage-amzn2-aarch64", "description": "(CDT) SELinux binary policy manipulation library" }, + { "name": "libsentencepiece", "uri": "https://anaconda.org/anaconda/libsentencepiece", "description": "Unsupervised text tokenizer for Neural Network-based text generation." }, + { "name": "libsepol-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsepol-amzn2-aarch64", "description": "(CDT) SELinux binary policy manipulation library" }, + { "name": "libsepol-cos6-i686", "uri": "https://anaconda.org/anaconda/libsepol-cos6-i686", "description": "(CDT) SELinux binary policy manipulation library" }, + { "name": "libsepol-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsepol-cos6-x86_64", "description": "(CDT) SELinux binary policy manipulation library" }, + { "name": "libsepol-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libsepol-cos7-ppc64le", "description": "(CDT) SELinux binary policy manipulation library" }, + { "name": "libsepol-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsepol-devel-amzn2-aarch64", "description": "(CDT) Header files and libraries used to build policy manipulation tools" }, + { "name": "libsepol-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libsepol-devel-cos6-i686", "description": "(CDT) Header files and libraries used to build policy manipulation tools" }, + { "name": "libsepol-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsepol-devel-cos6-x86_64", "description": "(CDT) Header files and libraries used to build policy manipulation tools" }, + { "name": "libsepol-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libsepol-devel-cos7-ppc64le", "description": "(CDT) Header files and libraries used to build policy manipulation tools" }, + { "name": "libsigcxx20-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsigcxx20-amzn2-aarch64", "description": "(CDT) Typesafe signal framework for C++" }, + { "name": "libsm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsm-amzn2-aarch64", "description": "(CDT) X.Org X11 SM runtime library" }, + { "name": "libsm-cos6-i686", "uri": "https://anaconda.org/anaconda/libsm-cos6-i686", "description": "(CDT) X.Org X11 SM runtime library" }, + { "name": "libsm-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsm-cos6-x86_64", "description": "(CDT) X.Org X11 SM runtime library" }, + { "name": "libsm-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libsm-cos7-ppc64le", "description": "(CDT) X.Org X11 SM runtime library" }, + { "name": "libsm-cos7-s390x", "uri": "https://anaconda.org/anaconda/libsm-cos7-s390x", "description": "(CDT) X.Org X11 SM runtime library" }, + { "name": "libsm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libsm-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 SM development package" }, + { "name": "libsm-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libsm-devel-cos6-i686", "description": "(CDT) X.Org X11 SM development package" }, + { "name": "libsm-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsm-devel-cos6-x86_64", "description": "(CDT) X.Org X11 SM development package" }, + { "name": "libsm-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libsm-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 SM development package" }, + { "name": "libsm-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libsm-devel-cos7-s390x", "description": "(CDT) X.Org X11 SM development package" }, + { "name": "libsolv", "uri": "https://anaconda.org/anaconda/libsolv", "description": "Library for solving packages and reading repositories" }, + { "name": "libsolv-static", "uri": "https://anaconda.org/anaconda/libsolv-static", "description": "Library for solving packages and reading repositories" }, + { "name": "libsoup-cos6-i686", "uri": "https://anaconda.org/anaconda/libsoup-cos6-i686", "description": "(CDT) Soup, an HTTP library implementation" }, + { "name": "libsoup-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsoup-cos6-x86_64", "description": "(CDT) Soup, an HTTP library implementation" }, + { "name": "libsoup-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libsoup-devel-cos6-i686", "description": "(CDT) Header files for the Soup library" }, + { "name": "libsoup-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libsoup-devel-cos6-x86_64", "description": "(CDT) Header files for the Soup library" }, + { "name": "libsoup-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libsoup-devel-cos7-s390x", "description": "(CDT) Header files for the Soup library" }, + { "name": "libspatialindex", "uri": "https://anaconda.org/anaconda/libspatialindex", "description": "Extensible framework for robust spatial indexing" }, + { "name": "libspatialite", "uri": "https://anaconda.org/anaconda/libspatialite", "description": "Extend the SQLite core to support fully fledged Spatial SQL capabilities" }, + { "name": "libssh2", "uri": "https://anaconda.org/anaconda/libssh2", "description": "the SSH library" }, + { "name": "libssh2-static", "uri": "https://anaconda.org/anaconda/libssh2-static", "description": "the SSH library" }, + { "name": "libstdcxx-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libstdcxx-amzn2-aarch64", "description": "(CDT) GNU Standard C++ Library" }, + { "name": "libstdcxx-devel_linux-64", "uri": "https://anaconda.org/anaconda/libstdcxx-devel_linux-64", "description": "The GNU C++ headers and development libraries" }, + { "name": "libstdcxx-devel_linux-aarch64", "uri": "https://anaconda.org/anaconda/libstdcxx-devel_linux-aarch64", "description": "The GNU C++ headers and development libraries" }, + { "name": "libstdcxx-devel_linux-ppc64le", "uri": "https://anaconda.org/anaconda/libstdcxx-devel_linux-ppc64le", "description": "The GNU C++ headers and development libraries" }, + { "name": "libstdcxx-devel_linux-s390x", "uri": "https://anaconda.org/anaconda/libstdcxx-devel_linux-s390x", "description": "The GNU C++ headers and development libraries" }, + { "name": "libstdcxx-ng", "uri": "https://anaconda.org/anaconda/libstdcxx-ng", "description": "The GNU C++ Runtime Library" }, + { "name": "libtasn1", "uri": "https://anaconda.org/anaconda/libtasn1", "description": "Libtasn1 is the ASN.1 library used by GnuTLS, p11-kit and some other packages" }, + { "name": "libtasn1-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libtasn1-amzn2-aarch64", "description": "(CDT) The ASN.1 library used in GNUTLS" }, + { "name": "libtasn1-cos6-i686", "uri": "https://anaconda.org/anaconda/libtasn1-cos6-i686", "description": "(CDT) The ASN.1 library used in GNUTLS" }, + { "name": "libtasn1-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libtasn1-cos6-x86_64", "description": "(CDT) The ASN.1 library used in GNUTLS" }, + { "name": "libtasn1-cos7-s390x", "uri": "https://anaconda.org/anaconda/libtasn1-cos7-s390x", "description": "(CDT) The ASN.1 library used in GNUTLS" }, + { "name": "libtensorflow", "uri": "https://anaconda.org/anaconda/libtensorflow", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "libtensorflow_cc", "uri": "https://anaconda.org/anaconda/libtensorflow_cc", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "libthai-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libthai-amzn2-aarch64", "description": "(CDT) Thai language support routines" }, + { "name": "libthai-cos6-i686", "uri": "https://anaconda.org/anaconda/libthai-cos6-i686", "description": "(CDT) Thai language support routines" }, + { "name": "libthai-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libthai-cos6-x86_64", "description": "(CDT) Thai language support routines" }, + { "name": "libthai-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libthai-cos7-ppc64le", "description": "(CDT) Thai language support routines" }, + { "name": "libtheora", "uri": "https://anaconda.org/anaconda/libtheora", "description": "Theora is a free and open video compression format from the Xiph.org Foundation." }, + { "name": "libthrift", "uri": "https://anaconda.org/anaconda/libthrift", "description": "Compiler and C++ libraries and headers for the Apache Thrift RPC system" }, + { "name": "libtiff", "uri": "https://anaconda.org/anaconda/libtiff", "description": "Support for the Tag Image File Format (TIFF)." }, + { "name": "libtiff-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libtiff-amzn2-aarch64", "description": "(CDT) Library of functions for manipulating TIFF format image files" }, + { "name": "libtiff-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libtiff-cos7-ppc64le", "description": "(CDT) Library of functions for manipulating TIFF format image files" }, + { "name": "libtiff-cos7-s390x", "uri": "https://anaconda.org/anaconda/libtiff-cos7-s390x", "description": "(CDT) Library of functions for manipulating TIFF format image files" }, + { "name": "libtmglib", "uri": "https://anaconda.org/anaconda/libtmglib", "description": "Linear Algebra PACKage" }, + { "name": "libudev-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libudev-cos6-x86_64", "description": "(CDT) Dynamic library to access udev device information" }, + { "name": "libunistring", "uri": "https://anaconda.org/anaconda/libunistring", "description": "This library provides functions for manipulating Unicode strings and for manipulating C strings according to the Unicode standard." }, + { "name": "libunistring-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libunistring-amzn2-aarch64", "description": "(CDT) GNU Unicode string library" }, + { "name": "libunwind", "uri": "https://anaconda.org/anaconda/libunwind", "description": "C++ Standard Library Support" }, + { "name": "libutf8proc", "uri": "https://anaconda.org/anaconda/libutf8proc", "description": "a clean C library for processing UTF-8 Unicode data" }, + { "name": "libuuid-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libuuid-amzn2-aarch64", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-cos6-i686", "uri": "https://anaconda.org/anaconda/libuuid-cos6-i686", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libuuid-cos6-x86_64", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libuuid-cos7-ppc64le", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-cos7-s390x", "uri": "https://anaconda.org/anaconda/libuuid-cos7-s390x", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libuuid-devel-cos6-i686", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libuuid-devel-cos6-x86_64", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libuuid-devel-cos7-ppc64le", "description": "(CDT) Universally unique ID library" }, + { "name": "libuuid-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libuuid-devel-cos7-s390x", "description": "(CDT) Universally unique ID library" }, + { "name": "libuv", "uri": "https://anaconda.org/anaconda/libuv", "description": "Cross-platform asynchronous I/O" }, + { "name": "libverto-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libverto-amzn2-aarch64", "description": "(CDT) Main loop abstraction library" }, + { "name": "libverto-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libverto-devel-amzn2-aarch64", "description": "(CDT) Development files for libverto" }, + { "name": "libvorbis", "uri": "https://anaconda.org/anaconda/libvorbis", "description": "Vorbis audio format" }, + { "name": "libvpx", "uri": "https://anaconda.org/anaconda/libvpx", "description": "A high-quality, open video format for the web" }, + { "name": "libwayland-client-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libwayland-client-amzn2-aarch64", "description": "(CDT) Wayland client library" }, + { "name": "libwayland-server-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libwayland-server-amzn2-aarch64", "description": "(CDT) Wayland server library" }, + { "name": "libwebp", "uri": "https://anaconda.org/anaconda/libwebp", "description": "WebP image library" }, + { "name": "libwebp-base", "uri": "https://anaconda.org/anaconda/libwebp-base", "description": "WebP image library" }, + { "name": "libx11-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libx11-amzn2-aarch64", "description": "(CDT) Core X11 protocol client library" }, + { "name": "libx11-common-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libx11-common-amzn2-aarch64", "description": "(CDT) Common data for libX11" }, + { "name": "libx11-common-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libx11-common-cos6-x86_64", "description": "(CDT) Common data for libX11" }, + { "name": "libx11-common-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libx11-common-cos7-ppc64le", "description": "(CDT) Common data for libX11" }, + { "name": "libx11-common-cos7-s390x", "uri": "https://anaconda.org/anaconda/libx11-common-cos7-s390x", "description": "(CDT) Common data for libX11" }, + { "name": "libx11-cos6-i686", "uri": "https://anaconda.org/anaconda/libx11-cos6-i686", "description": "(CDT) Core X11 protocol client library" }, + { "name": "libx11-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libx11-cos6-x86_64", "description": "(CDT) Core X11 protocol client library" }, + { "name": "libx11-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libx11-cos7-ppc64le", "description": "(CDT) Core X11 protocol client library" }, + { "name": "libx11-cos7-s390x", "uri": "https://anaconda.org/anaconda/libx11-cos7-s390x", "description": "(CDT) Core X11 protocol client library" }, + { "name": "libx11-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libx11-devel-amzn2-aarch64", "description": "(CDT) Development files for libX11" }, + { "name": "libx11-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libx11-devel-cos6-i686", "description": "(CDT) Development files for libX11" }, + { "name": "libx11-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libx11-devel-cos6-x86_64", "description": "(CDT) Development files for libX11" }, + { "name": "libx11-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libx11-devel-cos7-ppc64le", "description": "(CDT) Development files for libX11" }, + { "name": "libx11-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libx11-devel-cos7-s390x", "description": "(CDT) Development files for libX11" }, + { "name": "libxau-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxau-amzn2-aarch64", "description": "(CDT) Sample Authorization Protocol for X" }, + { "name": "libxau-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxau-cos6-x86_64", "description": "(CDT) Sample Authorization Protocol for X" }, + { "name": "libxau-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxau-cos7-ppc64le", "description": "(CDT) Sample Authorization Protocol for X" }, + { "name": "libxau-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxau-cos7-s390x", "description": "(CDT) Sample Authorization Protocol for X" }, + { "name": "libxau-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxau-devel-amzn2-aarch64", "description": "(CDT) Development files for libXau" }, + { "name": "libxau-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxau-devel-cos6-i686", "description": "(CDT) Development files for libXau" }, + { "name": "libxau-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxau-devel-cos7-ppc64le", "description": "(CDT) Development files for libXau" }, + { "name": "libxau-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxau-devel-cos7-s390x", "description": "(CDT) Development files for libXau" }, + { "name": "libxcb-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxcb-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "libxcb-cos6-i686", "uri": "https://anaconda.org/anaconda/libxcb-cos6-i686", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "libxcb-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxcb-cos7-ppc64le", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "libxcb-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxcb-cos7-s390x", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "libxcomposite-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxcomposite-amzn2-aarch64", "description": "(CDT) X Composite Extension library" }, + { "name": "libxcomposite-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxcomposite-cos6-x86_64", "description": "(CDT) X Composite Extension library" }, + { "name": "libxcomposite-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxcomposite-cos7-s390x", "description": "(CDT) X Composite Extension library" }, + { "name": "libxcomposite-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxcomposite-devel-amzn2-aarch64", "description": "(CDT) Development files for libXcomposite" }, + { "name": "libxcomposite-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxcomposite-devel-cos6-x86_64", "description": "(CDT) Development files for libXcomposite" }, + { "name": "libxcomposite-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxcomposite-devel-cos7-s390x", "description": "(CDT) Development files for libXcomposite" }, + { "name": "libxcursor-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxcursor-amzn2-aarch64", "description": "(CDT) Cursor management library" }, + { "name": "libxcursor-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxcursor-cos7-s390x", "description": "(CDT) Cursor management library" }, + { "name": "libxcursor-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxcursor-devel-amzn2-aarch64", "description": "(CDT) Development files for libXcursor" }, + { "name": "libxcursor-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxcursor-devel-cos6-i686", "description": "(CDT) Development files for libXcursor" }, + { "name": "libxcursor-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxcursor-devel-cos7-s390x", "description": "(CDT) Development files for libXcursor" }, + { "name": "libxdamage-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxdamage-amzn2-aarch64", "description": "(CDT) X Damage extension library" }, + { "name": "libxdamage-cos6-i686", "uri": "https://anaconda.org/anaconda/libxdamage-cos6-i686", "description": "(CDT) X Damage extension library" }, + { "name": "libxdamage-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxdamage-cos6-x86_64", "description": "(CDT) X Damage extension library" }, + { "name": "libxdamage-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxdamage-cos7-ppc64le", "description": "(CDT) X Damage extension library" }, + { "name": "libxdamage-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxdamage-cos7-s390x", "description": "(CDT) X Damage extension library" }, + { "name": "libxdamage-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxdamage-devel-amzn2-aarch64", "description": "(CDT) Development files for libXdamage" }, + { "name": "libxdamage-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxdamage-devel-cos6-i686", "description": "(CDT) Development files for libXdamage" }, + { "name": "libxdamage-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxdamage-devel-cos6-x86_64", "description": "(CDT) Development files for libXdamage" }, + { "name": "libxdamage-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxdamage-devel-cos7-ppc64le", "description": "(CDT) Development files for libXdamage" }, + { "name": "libxdamage-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxdamage-devel-cos7-s390x", "description": "(CDT) Development files for libXdamage" }, + { "name": "libxext-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxext-amzn2-aarch64", "description": "(CDT) X.Org X11 libXext runtime library" }, + { "name": "libxext-cos6-i686", "uri": "https://anaconda.org/anaconda/libxext-cos6-i686", "description": "(CDT) X.Org X11 libXext runtime library" }, + { "name": "libxext-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxext-cos6-x86_64", "description": "(CDT) X.Org X11 libXext runtime library" }, + { "name": "libxext-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxext-cos7-ppc64le", "description": "(CDT) X.Org X11 libXext runtime library" }, + { "name": "libxext-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxext-cos7-s390x", "description": "(CDT) X.Org X11 libXext runtime library" }, + { "name": "libxext-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxext-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXext development package" }, + { "name": "libxext-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxext-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXext development package" }, + { "name": "libxext-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxext-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXext development package" }, + { "name": "libxext-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxext-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXext development package" }, + { "name": "libxfixes-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxfixes-amzn2-aarch64", "description": "(CDT) X Fixes library" }, + { "name": "libxfixes-cos6-i686", "uri": "https://anaconda.org/anaconda/libxfixes-cos6-i686", "description": "(CDT) X Fixes library" }, + { "name": "libxfixes-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxfixes-cos7-ppc64le", "description": "(CDT) X Fixes library" }, + { "name": "libxfixes-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxfixes-cos7-s390x", "description": "(CDT) X Fixes library" }, + { "name": "libxfixes-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxfixes-devel-amzn2-aarch64", "description": "(CDT) Development files for libXfixes" }, + { "name": "libxfixes-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxfixes-devel-cos6-i686", "description": "(CDT) Development files for libXfixes" }, + { "name": "libxfixes-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxfixes-devel-cos6-x86_64", "description": "(CDT) Development files for libXfixes" }, + { "name": "libxfixes-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxfixes-devel-cos7-ppc64le", "description": "(CDT) Development files for libXfixes" }, + { "name": "libxfixes-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxfixes-devel-cos7-s390x", "description": "(CDT) Development files for libXfixes" }, + { "name": "libxft-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxft-amzn2-aarch64", "description": "(CDT) X.Org X11 libXft runtime library" }, + { "name": "libxft-cos6-i686", "uri": "https://anaconda.org/anaconda/libxft-cos6-i686", "description": "(CDT) X.Org X11 libXft runtime library" }, + { "name": "libxft-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxft-cos6-x86_64", "description": "(CDT) X.Org X11 libXft runtime library" }, + { "name": "libxft-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxft-cos7-ppc64le", "description": "(CDT) X.Org X11 libXft runtime library" }, + { "name": "libxgboost", "uri": "https://anaconda.org/anaconda/libxgboost", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "libxi-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxi-amzn2-aarch64", "description": "(CDT) X.Org X11 libXi runtime library" }, + { "name": "libxi-cos6-i686", "uri": "https://anaconda.org/anaconda/libxi-cos6-i686", "description": "(CDT) X.Org X11 libXi runtime library" }, + { "name": "libxi-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxi-cos6-x86_64", "description": "(CDT) X.Org X11 libXi runtime library" }, + { "name": "libxi-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxi-cos7-ppc64le", "description": "(CDT) X.Org X11 libXi runtime library" }, + { "name": "libxi-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxi-cos7-s390x", "description": "(CDT) X.Org X11 libXi runtime library" }, + { "name": "libxi-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxi-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXi development package" }, + { "name": "libxi-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxi-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXi development package" }, + { "name": "libxi-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxi-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXi development package" }, + { "name": "libxi-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxi-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXi development package" }, + { "name": "libxinerama-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxinerama-amzn2-aarch64", "description": "(CDT) X.Org X11 libXinerama runtime library" }, + { "name": "libxinerama-cos6-i686", "uri": "https://anaconda.org/anaconda/libxinerama-cos6-i686", "description": "(CDT) X.Org X11 libXinerama runtime library" }, + { "name": "libxinerama-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxinerama-cos6-x86_64", "description": "(CDT) X.Org X11 libXinerama runtime library" }, + { "name": "libxinerama-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxinerama-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXinerama development package" }, + { "name": "libxinerama-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxinerama-devel-cos6-i686", "description": "(CDT) X.Org X11 libXinerama development package" }, + { "name": "libxinerama-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxinerama-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXinerama development package" }, + { "name": "libxkbcommon", "uri": "https://anaconda.org/anaconda/libxkbcommon", "description": "keymap handling library for toolkits and window systems" }, + { "name": "libxkbfile-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxkbfile-amzn2-aarch64", "description": "(CDT) X.Org X11 libxkbfile development package" }, + { "name": "libxkbfile-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxkbfile-cos6-x86_64", "description": "(CDT) X.Org X11 libxkbfile runtime library" }, + { "name": "libxkbfile-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxkbfile-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libxkbfile development package" }, + { "name": "libxkbfile-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxkbfile-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libxkbfile development package" }, + { "name": "libxml2", "uri": "https://anaconda.org/anaconda/libxml2", "description": "The XML C parser and toolkit of Gnome" }, + { "name": "libxml2-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxml2-amzn2-aarch64", "description": "(CDT) Library providing XML and HTML support" }, + { "name": "libxml2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxml2-cos6-x86_64", "description": "(CDT) Library providing XML and HTML support" }, + { "name": "libxml2-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxml2-devel-cos6-x86_64", "description": "(CDT) Libraries, includes, etc. to develop XML and HTML applications" }, + { "name": "libxmlsec1", "uri": "https://anaconda.org/anaconda/libxmlsec1", "description": "XML Security Library" }, + { "name": "libxrandr-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxrandr-amzn2-aarch64", "description": "(CDT) X.Org X11 libXrandr runtime library" }, + { "name": "libxrandr-cos6-i686", "uri": "https://anaconda.org/anaconda/libxrandr-cos6-i686", "description": "(CDT) X.Org X11 libXrandr runtime library" }, + { "name": "libxrandr-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxrandr-cos7-ppc64le", "description": "(CDT) X.Org X11 libXrandr runtime library" }, + { "name": "libxrandr-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxrandr-cos7-s390x", "description": "(CDT) X.Org X11 libXrandr runtime library" }, + { "name": "libxrandr-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxrandr-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXrandr development package" }, + { "name": "libxrandr-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxrandr-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXrandr development package" }, + { "name": "libxrandr-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxrandr-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXrandr development package" }, + { "name": "libxrandr-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxrandr-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXrandr development package" }, + { "name": "libxrender-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxrender-amzn2-aarch64", "description": "(CDT) X.Org X11 libXrender runtime library" }, + { "name": "libxrender-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxrender-cos7-ppc64le", "description": "(CDT) X.Org X11 libXrender runtime library" }, + { "name": "libxrender-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxrender-cos7-s390x", "description": "(CDT) X.Org X11 libXrender runtime library" }, + { "name": "libxrender-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxrender-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXrender development package" }, + { "name": "libxrender-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxrender-devel-cos6-i686", "description": "(CDT) X.Org X11 libXrender development package" }, + { "name": "libxrender-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxrender-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXrender development package" }, + { "name": "libxrender-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxrender-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXrender development package" }, + { "name": "libxrender-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxrender-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXrender development package" }, + { "name": "libxscrnsaver-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxscrnsaver-amzn2-aarch64", "description": "(CDT) X.Org X11 libXss runtime library" }, + { "name": "libxscrnsaver-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxscrnsaver-cos7-s390x", "description": "(CDT) X.Org X11 libXss runtime library" }, + { "name": "libxscrnsaver-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxscrnsaver-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXScrnSaver development package" }, + { "name": "libxscrnsaver-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxscrnsaver-devel-cos6-i686", "description": "(CDT) X.Org X11 libXScrnSaver development package" }, + { "name": "libxscrnsaver-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxscrnsaver-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXScrnSaver development package" }, + { "name": "libxshmfence-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxshmfence-amzn2-aarch64", "description": "(CDT) X11 shared memory fences" }, + { "name": "libxshmfence-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxshmfence-cos7-ppc64le", "description": "(CDT) X11 shared memory fences" }, + { "name": "libxshmfence-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxshmfence-devel-cos7-ppc64le", "description": "(CDT) Development files for libxshmfence" }, + { "name": "libxslt", "uri": "https://anaconda.org/anaconda/libxslt", "description": "The XSLT C library developed for the GNOME project" }, + { "name": "libxt-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxt-amzn2-aarch64", "description": "(CDT) X.Org X11 libXt runtime library" }, + { "name": "libxt-cos6-i686", "uri": "https://anaconda.org/anaconda/libxt-cos6-i686", "description": "(CDT) X.Org X11 libXt runtime library" }, + { "name": "libxt-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxt-cos6-x86_64", "description": "(CDT) X.Org X11 libXt runtime library" }, + { "name": "libxt-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxt-cos7-ppc64le", "description": "(CDT) X.Org X11 libXt runtime library" }, + { "name": "libxt-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxt-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXt development package" }, + { "name": "libxt-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxt-devel-cos6-i686", "description": "(CDT) X.Org X11 libXt development package" }, + { "name": "libxt-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxt-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXt development package" }, + { "name": "libxt-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxt-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXt runtime library" }, + { "name": "libxtst-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxtst-amzn2-aarch64", "description": "(CDT) X.Org X11 libXtst runtime library" }, + { "name": "libxtst-cos6-i686", "uri": "https://anaconda.org/anaconda/libxtst-cos6-i686", "description": "(CDT) X.Org X11 libXtst runtime library" }, + { "name": "libxtst-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxtst-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXtst development package" }, + { "name": "libxtst-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxtst-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXtst development package" }, + { "name": "libxxf86vm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxxf86vm-amzn2-aarch64", "description": "(CDT) X.Org X11 libXxf86vm runtime library" }, + { "name": "libxxf86vm-cos6-i686", "uri": "https://anaconda.org/anaconda/libxxf86vm-cos6-i686", "description": "(CDT) X.Org X11 libXxf86vm runtime library" }, + { "name": "libxxf86vm-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxxf86vm-cos6-x86_64", "description": "(CDT) X.Org X11 libXxf86vm runtime library" }, + { "name": "libxxf86vm-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxxf86vm-cos7-ppc64le", "description": "(CDT) X.Org X11 libXxf86vm runtime library" }, + { "name": "libxxf86vm-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxxf86vm-cos7-s390x", "description": "(CDT) X.Org X11 libXxf86vm runtime library" }, + { "name": "libxxf86vm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/libxxf86vm-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 libXxf86vm development package" }, + { "name": "libxxf86vm-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/libxxf86vm-devel-cos6-i686", "description": "(CDT) X.Org X11 libXxf86vm development package" }, + { "name": "libxxf86vm-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/libxxf86vm-devel-cos6-x86_64", "description": "(CDT) X.Org X11 libXxf86vm development package" }, + { "name": "libxxf86vm-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/libxxf86vm-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 libXxf86vm development package" }, + { "name": "libxxf86vm-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/libxxf86vm-devel-cos7-s390x", "description": "(CDT) X.Org X11 libXxf86vm development package" }, + { "name": "libzopfli", "uri": "https://anaconda.org/anaconda/libzopfli", "description": "A compression library programmed in C to perform very good, but slow, deflate or zlib compression." }, + { "name": "license-expression", "uri": "https://anaconda.org/anaconda/license-expression", "description": "license-expression is small utility library to parse, compare, simplify and normalize license expressions (such as SPDX license expressions) using boolean logic." }, + { "name": "lightgbm", "uri": "https://anaconda.org/anaconda/lightgbm", "description": "LightGBM is a gradient boosting framework that uses tree based learning algorithms." }, + { "name": "lightning", "uri": "https://anaconda.org/anaconda/lightning", "description": "Use Lightning Apps to build everything from production-ready, multi-cloud ML systems to simple research demos." }, + { "name": "lightning-cloud", "uri": "https://anaconda.org/anaconda/lightning-cloud", "description": "Lightning AI Command Line Interface" }, + { "name": "lightning-utilities", "uri": "https://anaconda.org/anaconda/lightning-utilities", "description": "PyTorch Lightning Sample project." }, + { "name": "lime", "uri": "https://anaconda.org/anaconda/lime", "description": "Explaining the predictions of any machine learning classifier" }, + { "name": "line_profiler", "uri": "https://anaconda.org/anaconda/line_profiler", "description": "Line-by-line profiling for Python" }, + { "name": "linkify-it-py", "uri": "https://anaconda.org/anaconda/linkify-it-py", "description": "Links recognition library with FULL unicode support." }, + { "name": "lit", "uri": "https://anaconda.org/anaconda/lit", "description": "Development headers and libraries for LLVM" }, + { "name": "lld", "uri": "https://anaconda.org/anaconda/lld", "description": "The LLVM Linker" }, + { "name": "llvm", "uri": "https://anaconda.org/anaconda/llvm", "description": "Development headers and libraries for LLVM" }, + { "name": "llvm-lto-tapi", "uri": "https://anaconda.org/anaconda/llvm-lto-tapi", "description": "No Summary" }, + { "name": "llvm-openmp", "uri": "https://anaconda.org/anaconda/llvm-openmp", "description": "The OpenMP API supports multi-platform shared-memory parallel programming in C/C++ and Fortran." }, + { "name": "llvm-private-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/llvm-private-amzn2-aarch64", "description": "(CDT) llvm engine for Mesa" }, + { "name": "llvm-spirv", "uri": "https://anaconda.org/anaconda/llvm-spirv", "description": "A tool and a library for bi-directional translation between SPIR-V and LLVM IR" }, + { "name": "llvm-tools", "uri": "https://anaconda.org/anaconda/llvm-tools", "description": "Development headers and libraries for LLVM" }, + { "name": "llvmdev", "uri": "https://anaconda.org/anaconda/llvmdev", "description": "Development headers and libraries for LLVM" }, + { "name": "llvmlite", "uri": "https://anaconda.org/anaconda/llvmlite", "description": "A lightweight LLVM python binding for writing JIT compilers." }, + { "name": "lmdb", "uri": "https://anaconda.org/anaconda/lmdb", "description": "A high-performance embedded transactional key-value store database." }, + { "name": "locket", "uri": "https://anaconda.org/anaconda/locket", "description": "File-based locks for Python for Linux and Windows" }, + { "name": "lockfile", "uri": "https://anaconda.org/anaconda/lockfile", "description": "No Summary" }, + { "name": "locustio", "uri": "https://anaconda.org/anaconda/locustio", "description": "Website load testing framework" }, + { "name": "logbook", "uri": "https://anaconda.org/anaconda/logbook", "description": "Logbook is a nice logging replacement" }, + { "name": "logical-unification", "uri": "https://anaconda.org/anaconda/logical-unification", "description": "Logical unification in Python." }, + { "name": "loguru", "uri": "https://anaconda.org/anaconda/loguru", "description": "Python logging made (stupidly) simple" }, + { "name": "loky", "uri": "https://anaconda.org/anaconda/loky", "description": "Robust and reusable Executor for joblib" }, + { "name": "ltrace_linux-64", "uri": "https://anaconda.org/anaconda/ltrace_linux-64", "description": "Ltrace is a debugging tool for recording library calls, and signals" }, + { "name": "ltrace_linux-aarch64", "uri": "https://anaconda.org/anaconda/ltrace_linux-aarch64", "description": "Ltrace is a debugging tool for recording library calls, and signals" }, + { "name": "ltrace_linux-ppc64le", "uri": "https://anaconda.org/anaconda/ltrace_linux-ppc64le", "description": "Ltrace is a debugging tool for recording library calls, and signals" }, + { "name": "ltrace_linux-s390x", "uri": "https://anaconda.org/anaconda/ltrace_linux-s390x", "description": "Ltrace is a debugging tool for recording library calls, and signals" }, + { "name": "lua", "uri": "https://anaconda.org/anaconda/lua", "description": "Lua is a powerful, fast, lightweight, embeddable scripting language" }, + { "name": "lua-resty-http", "uri": "https://anaconda.org/anaconda/lua-resty-http", "description": "Lua HTTP client cosocket driver for OpenResty / ngx_lua." }, + { "name": "luajit", "uri": "https://anaconda.org/anaconda/luajit", "description": "Just-In-Time Compiler (JIT) for the Lua programming language." }, + { "name": "luarocks", "uri": "https://anaconda.org/anaconda/luarocks", "description": "LuaRocks is the package manager for Lua modulesLuaRocks is the package manager for Lua module" }, + { "name": "luigi", "uri": "https://anaconda.org/anaconda/luigi", "description": "Workflow mgmgt + task scheduling + dependency resolution." }, + { "name": "lunarcalendar", "uri": "https://anaconda.org/anaconda/lunarcalendar", "description": "A lunar calendar converter, including a number of lunar and solar holidays, mainly from China." }, + { "name": "lxml", "uri": "https://anaconda.org/anaconda/lxml", "description": "Pythonic binding for the C libraries libxml2 and libxslt." }, + { "name": "lz4", "uri": "https://anaconda.org/anaconda/lz4", "description": "LZ4 Bindings for Python" }, + { "name": "lz4-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/lz4-amzn2-aarch64", "description": "(CDT) Extremely fast compression algorithm" }, + { "name": "lz4-c", "uri": "https://anaconda.org/anaconda/lz4-c", "description": "Extremely Fast Compression algorithm" }, + { "name": "lz4-c-static", "uri": "https://anaconda.org/anaconda/lz4-c-static", "description": "Extremely Fast Compression algorithm" }, + { "name": "m2cgen", "uri": "https://anaconda.org/anaconda/m2cgen", "description": "Code-generation for various ML models into native code." }, + { "name": "m2w64-toolchain_win-32", "uri": "https://anaconda.org/anaconda/m2w64-toolchain_win-32", "description": "A meta-package to enable the right toolchain." }, + { "name": "m2w64-toolchain_win-64", "uri": "https://anaconda.org/anaconda/m2w64-toolchain_win-64", "description": "A meta-package to enable the right toolchain." }, + { "name": "m4ri", "uri": "https://anaconda.org/anaconda/m4ri", "description": "M4RI is a library for fast arithmetic with dense matrices over F2" }, + { "name": "macfsevents", "uri": "https://anaconda.org/anaconda/macfsevents", "description": "Thread-based interface to file system observation primitives." }, + { "name": "macholib", "uri": "https://anaconda.org/anaconda/macholib", "description": "Mach-O header analysis and editing" }, + { "name": "macports-legacy-support", "uri": "https://anaconda.org/anaconda/macports-legacy-support", "description": "Installs wrapper headers to add missing functionality to legacy OSX versions." }, + { "name": "magma", "uri": "https://anaconda.org/anaconda/magma", "description": "Matrix Algebra on GPU and Multicore Architectures" }, + { "name": "makedev-cos6-x86_64", "uri": "https://anaconda.org/anaconda/makedev-cos6-x86_64", "description": "(CDT) A program used for creating device files in /dev" }, + { "name": "mako", "uri": "https://anaconda.org/anaconda/mako", "description": "A super-fast templating language that borrows the best ideas from the existing templating languages." }, + { "name": "mando", "uri": "https://anaconda.org/anaconda/mando", "description": "Create Python CLI apps with little to no effort at all!" }, + { "name": "mapclassify", "uri": "https://anaconda.org/anaconda/mapclassify", "description": "Classification schemes for choropleth maps" }, + { "name": "markdown", "uri": "https://anaconda.org/anaconda/markdown", "description": "Python implementation of Markdown." }, + { "name": "markdown-it-py", "uri": "https://anaconda.org/anaconda/markdown-it-py", "description": "Python port of markdown-it. Markdown parsing, done right!" }, + { "name": "markdownlit", "uri": "https://anaconda.org/anaconda/markdownlit", "description": "markdownlit adds a couple of lit Markdown capabilities to your Streamlit apps" }, + { "name": "markupsafe", "uri": "https://anaconda.org/anaconda/markupsafe", "description": "Safely add untrusted strings to HTML/XML markup." }, + { "name": "marshmallow", "uri": "https://anaconda.org/anaconda/marshmallow", "description": "A lightweight library for converting complex datatypes to and from native Python datatypes." }, + { "name": "marshmallow-enum", "uri": "https://anaconda.org/anaconda/marshmallow-enum", "description": "Enum handling for Marshmallow" }, + { "name": "marshmallow-oneofschema", "uri": "https://anaconda.org/anaconda/marshmallow-oneofschema", "description": "Marshmallow library extension that allows schema (de)multiplexing" }, + { "name": "marshmallow-sqlalchemy", "uri": "https://anaconda.org/anaconda/marshmallow-sqlalchemy", "description": "SQLAlchemy integration with marshmallow" }, + { "name": "mashumaro", "uri": "https://anaconda.org/anaconda/mashumaro", "description": "Fast serialization framework on top of dataclasses" }, + { "name": "matplotlib", "uri": "https://anaconda.org/anaconda/matplotlib", "description": "Publication quality figures in Python" }, + { "name": "matplotlib-base", "uri": "https://anaconda.org/anaconda/matplotlib-base", "description": "Publication quality figures in Python" }, + { "name": "matplotlib-inline", "uri": "https://anaconda.org/anaconda/matplotlib-inline", "description": "Inline Matplotlib backend for Jupyter" }, + { "name": "matrixprofile", "uri": "https://anaconda.org/anaconda/matrixprofile", "description": "An open source time series data mining library based on Matrix Profile algorithms." }, + { "name": "maturin", "uri": "https://anaconda.org/anaconda/maturin", "description": "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" }, + { "name": "maturin-gnu", "uri": "https://anaconda.org/anaconda/maturin-gnu", "description": "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" }, + { "name": "maven", "uri": "https://anaconda.org/anaconda/maven", "description": "A software project management and comprehension tool." }, + { "name": "mdit-py-plugins", "uri": "https://anaconda.org/anaconda/mdit-py-plugins", "description": "Collection of plugins for markdown-it-py" }, + { "name": "mdp", "uri": "https://anaconda.org/anaconda/mdp", "description": "No Summary" }, + { "name": "mdurl", "uri": "https://anaconda.org/anaconda/mdurl", "description": "URL utilities for markdown-it-py parser." }, + { "name": "medspacy_quickumls", "uri": "https://anaconda.org/anaconda/medspacy_quickumls", "description": "QuickUMLS is a tool for fast, unsupervised biomedical concept extraction from medical text" }, + { "name": "menuinst", "uri": "https://anaconda.org/anaconda/menuinst", "description": "cross platform install of menu items" }, + { "name": "mesa-dri-drivers-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-dri-drivers-amzn2-aarch64", "description": "(CDT) Mesa-based DRI drivers" }, + { "name": "mesa-dri-drivers-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-dri-drivers-cos6-x86_64", "description": "(CDT) Mesa-based DRI drivers" }, + { "name": "mesa-dri-drivers-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/mesa-dri-drivers-cos7-ppc64le", "description": "(CDT) Mesa-based DRI drivers" }, + { "name": "mesa-dri-drivers-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-dri-drivers-cos7-s390x", "description": "(CDT) Mesa-based DRI drivers" }, + { "name": "mesa-dri1-drivers-cos6-i686", "uri": "https://anaconda.org/anaconda/mesa-dri1-drivers-cos6-i686", "description": "(CDT) Mesa graphics libraries" }, + { "name": "mesa-dri1-drivers-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-dri1-drivers-cos6-x86_64", "description": "(CDT) Mesa graphics libraries" }, + { "name": "mesa-filesystem-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-filesystem-amzn2-aarch64", "description": "(CDT) Mesa driver filesystem" }, + { "name": "mesa-khr-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-khr-devel-amzn2-aarch64", "description": "(CDT) Mesa Khronos development headers" }, + { "name": "mesa-khr-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/mesa-khr-devel-cos7-ppc64le", "description": "(CDT) Mesa Khronos development headers" }, + { "name": "mesa-libegl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libegl-amzn2-aarch64", "description": "(CDT) Mesa libEGL runtime libraries" }, + { "name": "mesa-libegl-cos6-i686", "uri": "https://anaconda.org/anaconda/mesa-libegl-cos6-i686", "description": "(CDT) Mesa libEGL runtime libraries" }, + { "name": "mesa-libegl-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-libegl-cos6-x86_64", "description": "(CDT) Mesa libEGL runtime libraries" }, + { "name": "mesa-libegl-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-libegl-cos7-s390x", "description": "(CDT) Mesa libEGL runtime libraries" }, + { "name": "mesa-libegl-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libegl-devel-amzn2-aarch64", "description": "(CDT) Mesa libEGL development package" }, + { "name": "mesa-libegl-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/mesa-libegl-devel-cos6-i686", "description": "(CDT) Mesa libEGL development package" }, + { "name": "mesa-libegl-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-libegl-devel-cos7-s390x", "description": "(CDT) Mesa libEGL development package" }, + { "name": "mesa-libgbm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libgbm-amzn2-aarch64", "description": "(CDT) Mesa gbm library" }, + { "name": "mesa-libgbm-cos6-i686", "uri": "https://anaconda.org/anaconda/mesa-libgbm-cos6-i686", "description": "(CDT) Mesa gbm library" }, + { "name": "mesa-libgbm-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-libgbm-cos6-x86_64", "description": "(CDT) Mesa gbm library" }, + { "name": "mesa-libgbm-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-libgbm-cos7-s390x", "description": "(CDT) Mesa gbm library" }, + { "name": "mesa-libgbm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libgbm-devel-amzn2-aarch64", "description": "(CDT) Mesa gbm development package" }, + { "name": "mesa-libgbm-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-libgbm-devel-cos6-x86_64", "description": "(CDT) Mesa gbm development package" }, + { "name": "mesa-libgl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libgl-amzn2-aarch64", "description": "(CDT) Mesa libGL runtime libraries and DRI drivers" }, + { "name": "mesa-libgl-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/mesa-libgl-cos7-ppc64le", "description": "(CDT) Mesa libGL runtime libraries and DRI drivers" }, + { "name": "mesa-libgl-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-libgl-cos7-s390x", "description": "(CDT) Mesa libGL runtime libraries and DRI drivers" }, + { "name": "mesa-libgl-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libgl-devel-amzn2-aarch64", "description": "(CDT) Mesa libGL development package" }, + { "name": "mesa-libgl-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/mesa-libgl-devel-cos6-i686", "description": "(CDT) Mesa libGL development package" }, + { "name": "mesa-libgl-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/mesa-libgl-devel-cos6-x86_64", "description": "(CDT) Mesa libGL development package" }, + { "name": "mesa-libgl-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/mesa-libgl-devel-cos7-ppc64le", "description": "(CDT) Mesa libGL development package" }, + { "name": "mesa-libglapi-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/mesa-libglapi-amzn2-aarch64", "description": "(CDT) Mesa shared glapi" }, + { "name": "mesa-libglapi-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/mesa-libglapi-cos7-ppc64le", "description": "(CDT) Mesa shared glapi" }, + { "name": "mesa-libglapi-cos7-s390x", "uri": "https://anaconda.org/anaconda/mesa-libglapi-cos7-s390x", "description": "(CDT) Mesa shared glapi" }, + { "name": "meson", "uri": "https://anaconda.org/anaconda/meson", "description": "The Meson Build System" }, + { "name": "meson-python", "uri": "https://anaconda.org/anaconda/meson-python", "description": "Meson Python build backend (PEP 517)" }, + { "name": "metakernel", "uri": "https://anaconda.org/anaconda/metakernel", "description": "Metakernel for Jupyter." }, + { "name": "metis", "uri": "https://anaconda.org/anaconda/metis", "description": "METIS - Serial Graph Partitioning and Fill-reducing Matrix Ordering" }, + { "name": "mgwr", "uri": "https://anaconda.org/anaconda/mgwr", "description": "Multiscale geographically weighted regression" }, + { "name": "mingw64-gcc-toolchain", "uri": "https://anaconda.org/anaconda/mingw64-gcc-toolchain", "description": "MINGW64 GCC toolchain packages" }, + { "name": "mingw64-gcc-toolchain_win-64", "uri": "https://anaconda.org/anaconda/mingw64-gcc-toolchain_win-64", "description": "MINGW64 GCC toolchain metapackage" }, + { "name": "minikanren", "uri": "https://anaconda.org/anaconda/minikanren", "description": "An extensible, lightweight relational/logic programming DSL written in pure Python" }, + { "name": "minimal-snowplow-tracker", "uri": "https://anaconda.org/anaconda/minimal-snowplow-tracker", "description": "Snowplow event tracker for Python" }, + { "name": "minio", "uri": "https://anaconda.org/anaconda/minio", "description": "MinIO Python Library for Amazon S3 Compatible Cloud Storage for Python" }, + { "name": "minizip", "uri": "https://anaconda.org/anaconda/minizip", "description": "minizip-ng is a zip manipulation library written in C." }, + { "name": "missingno", "uri": "https://anaconda.org/anaconda/missingno", "description": "Missing data visualization module for Python." }, + { "name": "mistune", "uri": "https://anaconda.org/anaconda/mistune", "description": "A sane Markdown parser with useful plugins and renderers." }, + { "name": "mizani", "uri": "https://anaconda.org/anaconda/mizani", "description": "A scales package for python" }, + { "name": "mkl", "uri": "https://anaconda.org/anaconda/mkl", "description": "Math library for Intel and compatible processors" }, + { "name": "mkl-devel-dpcpp", "uri": "https://anaconda.org/anaconda/mkl-devel-dpcpp", "description": "Intel® oneAPI Math Kernel Library" }, + { "name": "mkl-dnn", "uri": "https://anaconda.org/anaconda/mkl-dnn", "description": "Intel(R) Math Kernel Library for Deep Neural Networks (Intel(R) MKL-DNN)" }, + { "name": "mkl-dpcpp", "uri": "https://anaconda.org/anaconda/mkl-dpcpp", "description": "Intel® oneAPI Math Kernel Library" }, + { "name": "mkl-include", "uri": "https://anaconda.org/anaconda/mkl-include", "description": "MKL headers for developing software that uses MKL" }, + { "name": "mkl-service", "uri": "https://anaconda.org/anaconda/mkl-service", "description": "Python hooks for Intel(R) Math Kernel Library runtime control settings." }, + { "name": "mkl_fft", "uri": "https://anaconda.org/anaconda/mkl_fft", "description": "NumPy-based implementation of Fast Fourier Transform using Intel (R) Math Kernel Library." }, + { "name": "mkl_random", "uri": "https://anaconda.org/anaconda/mkl_random", "description": "Intel (R) MKL-powered package for sampling from common probability distributions into NumPy arrays." }, + { "name": "mkl_umath", "uri": "https://anaconda.org/anaconda/mkl_umath", "description": "NumPy-based implementation of universal math functions using Intel(R) Math Kernel Library (Intel(R) MKL) and Intel(R) C Compiler." }, + { "name": "mklml", "uri": "https://anaconda.org/anaconda/mklml", "description": "No Summary" }, + { "name": "ml_dtypes", "uri": "https://anaconda.org/anaconda/ml_dtypes", "description": "A stand-alone implementation of several NumPy dtype extensions used in machine learning libraries" }, + { "name": "mlflow", "uri": "https://anaconda.org/anaconda/mlflow", "description": "MLflow: A Machine Learning Lifecycle Platform" }, + { "name": "mlflow-skinny", "uri": "https://anaconda.org/anaconda/mlflow-skinny", "description": "MLflow Skinny: A Lightweight Machine Learning Lifecycle Platform Client" }, + { "name": "mlir", "uri": "https://anaconda.org/anaconda/mlir", "description": "Multi-Level IR Compiler Framework" }, + { "name": "mlxtend", "uri": "https://anaconda.org/anaconda/mlxtend", "description": "Machine Learning Library Extensions" }, + { "name": "mmh3", "uri": "https://anaconda.org/anaconda/mmh3", "description": "Python wrapper for MurmurHash (MurmurHash3), a set of fast and robust hash functions." }, + { "name": "mockito", "uri": "https://anaconda.org/anaconda/mockito", "description": "Mockito is a spying framework." }, + { "name": "mockupdb", "uri": "https://anaconda.org/anaconda/mockupdb", "description": "MongoDB Wire Protocol server library" }, + { "name": "modin", "uri": "https://anaconda.org/anaconda/modin", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "modin-all", "uri": "https://anaconda.org/anaconda/modin-all", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "modin-core", "uri": "https://anaconda.org/anaconda/modin-core", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "modin-dask", "uri": "https://anaconda.org/anaconda/modin-dask", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "modin-omnisci", "uri": "https://anaconda.org/anaconda/modin-omnisci", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "modin-ray", "uri": "https://anaconda.org/anaconda/modin-ray", "description": "Speed up your Pandas workflows by changing a single line of code" }, + { "name": "mongo-tools", "uri": "https://anaconda.org/anaconda/mongo-tools", "description": "Tools for managing and monitoring MongoDB clusters" }, + { "name": "mongodb", "uri": "https://anaconda.org/anaconda/mongodb", "description": "A next-gen database that lets you do things you could never do before" }, + { "name": "mono", "uri": "https://anaconda.org/anaconda/mono", "description": "Mono is a software platform designed to allow developers to easily create cross platform applications." }, + { "name": "monotonic", "uri": "https://anaconda.org/anaconda/monotonic", "description": "An implementation of time.monotonic() for Python 2 & Python 3." }, + { "name": "more-itertools", "uri": "https://anaconda.org/anaconda/more-itertools", "description": "More routines for operating on iterables, beyond itertools" }, + { "name": "morfessor", "uri": "https://anaconda.org/anaconda/morfessor", "description": "Library for unsupervised and semi-supervised morphological segmentation" }, + { "name": "moto", "uri": "https://anaconda.org/anaconda/moto", "description": "A library that allows your python tests to easily mock out the boto library." }, + { "name": "mpg123", "uri": "https://anaconda.org/anaconda/mpg123", "description": "mpg123 - fast console MPEG Audio Player and decoder library" }, + { "name": "mpi", "uri": "https://anaconda.org/anaconda/mpi", "description": "A high performance widely portable implementation of the MPI standard." }, + { "name": "mpich", "uri": "https://anaconda.org/anaconda/mpich", "description": "A high performance widely portable implementation of the MPI standard." }, + { "name": "mpich-mpicc", "uri": "https://anaconda.org/anaconda/mpich-mpicc", "description": "A high performance widely portable implementation of the MPI standard." }, + { "name": "mpich-mpicxx", "uri": "https://anaconda.org/anaconda/mpich-mpicxx", "description": "A high performance widely portable implementation of the MPI standard." }, + { "name": "mpich-mpifort", "uri": "https://anaconda.org/anaconda/mpich-mpifort", "description": "A high performance widely portable implementation of the MPI standard." }, + { "name": "mpir", "uri": "https://anaconda.org/anaconda/mpir", "description": "Multiple Precision Integers and Rationals." }, + { "name": "mpl-scatter-density", "uri": "https://anaconda.org/anaconda/mpl-scatter-density", "description": "Matplotlib helpers to make density scatter plots" }, + { "name": "mpl_sample_data", "uri": "https://anaconda.org/anaconda/mpl_sample_data", "description": "Publication quality figures in Python" }, + { "name": "mpld3", "uri": "https://anaconda.org/anaconda/mpld3", "description": "D3 Viewer for Matplotlib." }, + { "name": "mpmath", "uri": "https://anaconda.org/anaconda/mpmath", "description": "Python library for arbitrary-precision floating-point arithmetic" }, + { "name": "msal", "uri": "https://anaconda.org/anaconda/msal", "description": "Microsoft Authentication Library (MSAL) for Python makes it easy to authenticate to Azure Active Directory" }, + { "name": "msgpack-numpy", "uri": "https://anaconda.org/anaconda/msgpack-numpy", "description": "Numpy data serialization using msgpack" }, + { "name": "msgpack-python", "uri": "https://anaconda.org/anaconda/msgpack-python", "description": "MessagePack (de)serializer" }, + { "name": "msitools", "uri": "https://anaconda.org/anaconda/msitools", "description": "msitools is a set of programs to inspect and build Windows Installer (.MSI) files" }, + { "name": "msmpi", "uri": "https://anaconda.org/anaconda/msmpi", "description": "Microsoft message-passing-interface (MS-MPI)" }, + { "name": "msrest", "uri": "https://anaconda.org/anaconda/msrest", "description": "The runtime library \"msrest\" for AutoRest generated Python clients." }, + { "name": "msvc-headers-libs", "uri": "https://anaconda.org/anaconda/msvc-headers-libs", "description": "Scripts to download MSVC headers and libraries" }, + { "name": "msys2-autoconf-wrapper", "uri": "https://anaconda.org/anaconda/msys2-autoconf-wrapper", "description": "(repack of MSYS2-packages autoconf-wrapper for MSYS)" }, + { "name": "msys2-autoconf2.13", "uri": "https://anaconda.org/anaconda/msys2-autoconf2.13", "description": "A GNU tool for automatically configuring source code (repack of MSYS2-packages autoconf2.13 for MSYS)" }, + { "name": "msys2-autoconf2.69", "uri": "https://anaconda.org/anaconda/msys2-autoconf2.69", "description": "A GNU tool for automatically configuring source code (repack of MSYS2-packages autoconf2.69 for MSYS)" }, + { "name": "msys2-autoconf2.71", "uri": "https://anaconda.org/anaconda/msys2-autoconf2.71", "description": "A GNU tool for automatically configuring source code (repack of MSYS2-packages autoconf2.71 for MSYS)" }, + { "name": "msys2-autoconf2.72", "uri": "https://anaconda.org/anaconda/msys2-autoconf2.72", "description": "A GNU tool for automatically configuring source code (repack of MSYS2-packages autoconf2.72 for MSYS)" }, + { "name": "msys2-automake-wrapper", "uri": "https://anaconda.org/anaconda/msys2-automake-wrapper", "description": "(repack of MSYS2-packages automake-wrapper for MSYS)" }, + { "name": "msys2-automake1.11", "uri": "https://anaconda.org/anaconda/msys2-automake1.11", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.11 for MSYS)" }, + { "name": "msys2-automake1.12", "uri": "https://anaconda.org/anaconda/msys2-automake1.12", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.12 for MSYS)" }, + { "name": "msys2-automake1.13", "uri": "https://anaconda.org/anaconda/msys2-automake1.13", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.13 for MSYS)" }, + { "name": "msys2-automake1.14", "uri": "https://anaconda.org/anaconda/msys2-automake1.14", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.14 for MSYS)" }, + { "name": "msys2-automake1.15", "uri": "https://anaconda.org/anaconda/msys2-automake1.15", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.15 for MSYS)" }, + { "name": "msys2-automake1.16", "uri": "https://anaconda.org/anaconda/msys2-automake1.16", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.16 for MSYS)" }, + { "name": "msys2-automake1.17", "uri": "https://anaconda.org/anaconda/msys2-automake1.17", "description": "A GNU tool for automatically creating Makefiles (repack of MSYS2-packages automake1.17 for MSYS)" }, + { "name": "msys2-autotools", "uri": "https://anaconda.org/anaconda/msys2-autotools", "description": "A meta package for the GNU autotools build system (repack of MSYS2-packages autotools for MSYS)" }, + { "name": "msys2-base", "uri": "https://anaconda.org/anaconda/msys2-base", "description": "MSYS base development packages" }, + { "name": "msys2-bash", "uri": "https://anaconda.org/anaconda/msys2-bash", "description": "The GNU Bourne Again shell (repack of MSYS2-packages bash for MSYS)" }, + { "name": "msys2-bash-completion", "uri": "https://anaconda.org/anaconda/msys2-bash-completion", "description": "Programmable completion for the bash shell (repack of MSYS2-packages bash-completion for MSYS)" }, + { "name": "msys2-bash-devel", "uri": "https://anaconda.org/anaconda/msys2-bash-devel", "description": "The GNU Bourne Again shell (repack of MSYS2-packages bash-devel for MSYS)" }, + { "name": "msys2-binutils", "uri": "https://anaconda.org/anaconda/msys2-binutils", "description": "A set of programs to assemble and manipulate binary and object files (repack of MSYS2-packages binutils for MSYS)" }, + { "name": "msys2-bison", "uri": "https://anaconda.org/anaconda/msys2-bison", "description": "The GNU general-purpose parser generator (repack of MSYS2-packages bison for MSYS)" }, + { "name": "msys2-brotli", "uri": "https://anaconda.org/anaconda/msys2-brotli", "description": "Brotli compression library (repack of MSYS2-packages brotli for MSYS)" }, + { "name": "msys2-brotli-devel", "uri": "https://anaconda.org/anaconda/msys2-brotli-devel", "description": "Brotli compression library (repack of MSYS2-packages brotli-devel for MSYS)" }, + { "name": "msys2-brotli-testdata", "uri": "https://anaconda.org/anaconda/msys2-brotli-testdata", "description": "Brotli compression library (repack of MSYS2-packages brotli-testdata for MSYS)" }, + { "name": "msys2-bzip2", "uri": "https://anaconda.org/anaconda/msys2-bzip2", "description": "A high-quality data compression program (repack of MSYS2-packages bzip2 for MSYS)" }, + { "name": "msys2-ca-certificates", "uri": "https://anaconda.org/anaconda/msys2-ca-certificates", "description": "Common CA certificates (repack of MSYS2-packages ca-certificates for MSYS)" }, + { "name": "msys2-coreutils", "uri": "https://anaconda.org/anaconda/msys2-coreutils", "description": "The basic file, shell and text manipulation utilities of the GNU operating system (repack of MSYS2-packages coreutils for MSYS)" }, + { "name": "msys2-curl", "uri": "https://anaconda.org/anaconda/msys2-curl", "description": "Multi-protocol file transfer utility (repack of MSYS2-packages curl for MSYS)" }, + { "name": "msys2-dash", "uri": "https://anaconda.org/anaconda/msys2-dash", "description": "A POSIX compliant shell that aims to be as small as possible (repack of MSYS2-packages dash for MSYS)" }, + { "name": "msys2-db", "uri": "https://anaconda.org/anaconda/msys2-db", "description": "The Berkeley DB embedded database system (repack of MSYS2-packages db for MSYS)" }, + { "name": "msys2-db-docs", "uri": "https://anaconda.org/anaconda/msys2-db-docs", "description": "The Berkeley DB embedded database system (repack of MSYS2-packages db-docs for MSYS)" }, + { "name": "msys2-diffutils", "uri": "https://anaconda.org/anaconda/msys2-diffutils", "description": "Utility programs used for creating patch files (repack of MSYS2-packages diffutils for MSYS)" }, + { "name": "msys2-expat", "uri": "https://anaconda.org/anaconda/msys2-expat", "description": "An XML parser library (repack of MSYS2-packages expat for MSYS)" }, + { "name": "msys2-fido2-tools", "uri": "https://anaconda.org/anaconda/msys2-fido2-tools", "description": "Library functionality for FIDO 2.0, including communication with a device over USB (repack of MSYS2-packages fido2-tools for MSYS)" }, + { "name": "msys2-file", "uri": "https://anaconda.org/anaconda/msys2-file", "description": "File type identification utility (repack of MSYS2-packages file for MSYS)" }, + { "name": "msys2-filesystem", "uri": "https://anaconda.org/anaconda/msys2-filesystem", "description": "Base filesystem (repack of MSYS2-packages filesystem for MSYS)" }, + { "name": "msys2-findutils", "uri": "https://anaconda.org/anaconda/msys2-findutils", "description": "GNU utilities to locate files (repack of MSYS2-packages findutils for MSYS)" }, + { "name": "msys2-flex", "uri": "https://anaconda.org/anaconda/msys2-flex", "description": "A tool for generating text-scanning programs (repack of MSYS2-packages flex for MSYS)" }, + { "name": "msys2-gawk", "uri": "https://anaconda.org/anaconda/msys2-gawk", "description": "GNU version of awk (repack of MSYS2-packages gawk for MSYS)" }, + { "name": "msys2-gcc", "uri": "https://anaconda.org/anaconda/msys2-gcc", "description": "The GNU Compiler Collection (repack of MSYS2-packages gcc for MSYS)" }, + { "name": "msys2-gcc-libs", "uri": "https://anaconda.org/anaconda/msys2-gcc-libs", "description": "The GNU Compiler Collection (repack of MSYS2-packages gcc-libs for MSYS)" }, + { "name": "msys2-gdbm", "uri": "https://anaconda.org/anaconda/msys2-gdbm", "description": "GNU database library (repack of MSYS2-packages gdbm for MSYS)" }, + { "name": "msys2-gettext", "uri": "https://anaconda.org/anaconda/msys2-gettext", "description": "GNU internationalization library (repack of MSYS2-packages gettext for MSYS)" }, + { "name": "msys2-gettext-devel", "uri": "https://anaconda.org/anaconda/msys2-gettext-devel", "description": "GNU internationalization library (repack of MSYS2-packages gettext-devel for MSYS)" }, + { "name": "msys2-git", "uri": "https://anaconda.org/anaconda/msys2-git", "description": "The fast distributed version control system (repack of MSYS2-packages git for MSYS)" }, + { "name": "msys2-gmp", "uri": "https://anaconda.org/anaconda/msys2-gmp", "description": "A free library for arbitrary precision arithmetic (repack of MSYS2-packages gmp for MSYS)" }, + { "name": "msys2-gmp-devel", "uri": "https://anaconda.org/anaconda/msys2-gmp-devel", "description": "A free library for arbitrary precision arithmetic (repack of MSYS2-packages gmp-devel for MSYS)" }, + { "name": "msys2-gperf", "uri": "https://anaconda.org/anaconda/msys2-gperf", "description": "Perfect hash function generator (repack of MSYS2-packages gperf for MSYS)" }, + { "name": "msys2-grep", "uri": "https://anaconda.org/anaconda/msys2-grep", "description": "A string search utility (repack of MSYS2-packages grep for MSYS)" }, + { "name": "msys2-gzip", "uri": "https://anaconda.org/anaconda/msys2-gzip", "description": "GNU compression utility (repack of MSYS2-packages gzip for MSYS)" }, + { "name": "msys2-heimdal", "uri": "https://anaconda.org/anaconda/msys2-heimdal", "description": "Implementation of Kerberos V5 libraries (repack of MSYS2-packages heimdal for MSYS)" }, + { "name": "msys2-heimdal-devel", "uri": "https://anaconda.org/anaconda/msys2-heimdal-devel", "description": "Implementation of Kerberos V5 libraries (repack of MSYS2-packages heimdal-devel for MSYS)" }, + { "name": "msys2-heimdal-libs", "uri": "https://anaconda.org/anaconda/msys2-heimdal-libs", "description": "Implementation of Kerberos V5 libraries (repack of MSYS2-packages heimdal-libs for MSYS)" }, + { "name": "msys2-inetutils", "uri": "https://anaconda.org/anaconda/msys2-inetutils", "description": "A collection of common network programs. (repack of MSYS2-packages inetutils for MSYS)" }, + { "name": "msys2-info", "uri": "https://anaconda.org/anaconda/msys2-info", "description": "Utilities to work with and produce manuals, ASCII text, and on-line documentation from a single source file (repack of MSYS2-packages info for MSYS)" }, + { "name": "msys2-isl", "uri": "https://anaconda.org/anaconda/msys2-isl", "description": "Library for manipulating sets and relations of integer points bounded by linear constraints (repack of MSYS2-packages isl for MSYS)" }, + { "name": "msys2-isl-devel", "uri": "https://anaconda.org/anaconda/msys2-isl-devel", "description": "Library for manipulating sets and relations of integer points bounded by linear constraints (repack of MSYS2-packages isl-devel for MSYS)" }, + { "name": "msys2-jansson", "uri": "https://anaconda.org/anaconda/msys2-jansson", "description": "C library for encoding, decoding and manipulating JSON data (repack of MSYS2-packages jansson for MSYS)" }, + { "name": "msys2-jansson-devel", "uri": "https://anaconda.org/anaconda/msys2-jansson-devel", "description": "C library for encoding, decoding and manipulating JSON data (repack of MSYS2-packages jansson-devel for MSYS)" }, + { "name": "msys2-lemon", "uri": "https://anaconda.org/anaconda/msys2-lemon", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages lemon for MSYS)" }, + { "name": "msys2-less", "uri": "https://anaconda.org/anaconda/msys2-less", "description": "A terminal based program for viewing text files (repack of MSYS2-packages less for MSYS)" }, + { "name": "msys2-libasprintf", "uri": "https://anaconda.org/anaconda/msys2-libasprintf", "description": "GNU internationalization library (repack of MSYS2-packages libasprintf for MSYS)" }, + { "name": "msys2-libbz2", "uri": "https://anaconda.org/anaconda/msys2-libbz2", "description": "A high-quality data compression program (repack of MSYS2-packages libbz2 for MSYS)" }, + { "name": "msys2-libbz2-devel", "uri": "https://anaconda.org/anaconda/msys2-libbz2-devel", "description": "A high-quality data compression program (repack of MSYS2-packages libbz2-devel for MSYS)" }, + { "name": "msys2-libcares", "uri": "https://anaconda.org/anaconda/msys2-libcares", "description": "C library that performs DNS requests and name resolves asynchronously (libraries) (repack of MSYS2-packages libcares for MSYS)" }, + { "name": "msys2-libcares-devel", "uri": "https://anaconda.org/anaconda/msys2-libcares-devel", "description": "C library that performs DNS requests and name resolves asynchronously (libraries) (repack of MSYS2-packages libcares-devel for MSYS)" }, + { "name": "msys2-libcbor", "uri": "https://anaconda.org/anaconda/msys2-libcbor", "description": "A C library for parsing and generating CBOR, a general-purpose schema-less binary data format (repack of MSYS2-packages libcbor for MSYS)" }, + { "name": "msys2-libcbor-devel", "uri": "https://anaconda.org/anaconda/msys2-libcbor-devel", "description": "A C library for parsing and generating CBOR, a general-purpose schema-less binary data format (repack of MSYS2-packages libcbor-devel for MSYS)" }, + { "name": "msys2-libcurl", "uri": "https://anaconda.org/anaconda/msys2-libcurl", "description": "Multi-protocol file transfer utility (repack of MSYS2-packages libcurl for MSYS)" }, + { "name": "msys2-libcurl-devel", "uri": "https://anaconda.org/anaconda/msys2-libcurl-devel", "description": "Multi-protocol file transfer utility (repack of MSYS2-packages libcurl-devel for MSYS)" }, + { "name": "msys2-libdb", "uri": "https://anaconda.org/anaconda/msys2-libdb", "description": "The Berkeley DB embedded database system (repack of MSYS2-packages libdb for MSYS)" }, + { "name": "msys2-libdb-devel", "uri": "https://anaconda.org/anaconda/msys2-libdb-devel", "description": "The Berkeley DB embedded database system (repack of MSYS2-packages libdb-devel for MSYS)" }, + { "name": "msys2-libedit", "uri": "https://anaconda.org/anaconda/msys2-libedit", "description": "Libedit is an autotool- and libtoolized port of the NetBSD Editline library. (repack of MSYS2-packages libedit for MSYS)" }, + { "name": "msys2-libedit-devel", "uri": "https://anaconda.org/anaconda/msys2-libedit-devel", "description": "Libedit is an autotool- and libtoolized port of the NetBSD Editline library. (repack of MSYS2-packages libedit-devel for MSYS)" }, + { "name": "msys2-libevent", "uri": "https://anaconda.org/anaconda/msys2-libevent", "description": "An event notification library (repack of MSYS2-packages libevent for MSYS)" }, + { "name": "msys2-libevent-devel", "uri": "https://anaconda.org/anaconda/msys2-libevent-devel", "description": "An event notification library (repack of MSYS2-packages libevent-devel for MSYS)" }, + { "name": "msys2-libexpat", "uri": "https://anaconda.org/anaconda/msys2-libexpat", "description": "An XML parser library (repack of MSYS2-packages libexpat for MSYS)" }, + { "name": "msys2-libexpat-devel", "uri": "https://anaconda.org/anaconda/msys2-libexpat-devel", "description": "An XML parser library (repack of MSYS2-packages libexpat-devel for MSYS)" }, + { "name": "msys2-libffi", "uri": "https://anaconda.org/anaconda/msys2-libffi", "description": "Portable, high level programming interface to various calling conventions (repack of MSYS2-packages libffi for MSYS)" }, + { "name": "msys2-libffi-devel", "uri": "https://anaconda.org/anaconda/msys2-libffi-devel", "description": "Portable, high level programming interface to various calling conventions (repack of MSYS2-packages libffi-devel for MSYS)" }, + { "name": "msys2-libfido2", "uri": "https://anaconda.org/anaconda/msys2-libfido2", "description": "Library functionality for FIDO 2.0, including communication with a device over USB (repack of MSYS2-packages libfido2 for MSYS)" }, + { "name": "msys2-libfido2-devel", "uri": "https://anaconda.org/anaconda/msys2-libfido2-devel", "description": "Library functionality for FIDO 2.0, including communication with a device over USB (repack of MSYS2-packages libfido2-devel for MSYS)" }, + { "name": "msys2-libfido2-docs", "uri": "https://anaconda.org/anaconda/msys2-libfido2-docs", "description": "Library functionality for FIDO 2.0, including communication with a device over USB (repack of MSYS2-packages libfido2-docs for MSYS)" }, + { "name": "msys2-libgdbm", "uri": "https://anaconda.org/anaconda/msys2-libgdbm", "description": "GNU database library (repack of MSYS2-packages libgdbm for MSYS)" }, + { "name": "msys2-libgdbm-devel", "uri": "https://anaconda.org/anaconda/msys2-libgdbm-devel", "description": "GNU database library (repack of MSYS2-packages libgdbm-devel for MSYS)" }, + { "name": "msys2-libgettextpo", "uri": "https://anaconda.org/anaconda/msys2-libgettextpo", "description": "GNU internationalization library (repack of MSYS2-packages libgettextpo for MSYS)" }, + { "name": "msys2-libiconv", "uri": "https://anaconda.org/anaconda/msys2-libiconv", "description": "Libiconv is a conversion library (repack of MSYS2-packages libiconv for MSYS)" }, + { "name": "msys2-libiconv-devel", "uri": "https://anaconda.org/anaconda/msys2-libiconv-devel", "description": "Libiconv is a conversion library (repack of MSYS2-packages libiconv-devel for MSYS)" }, + { "name": "msys2-libidn2", "uri": "https://anaconda.org/anaconda/msys2-libidn2", "description": "Implementation of the Stringprep, Punycode and IDNA specifications (repack of MSYS2-packages libidn2 for MSYS)" }, + { "name": "msys2-libidn2-devel", "uri": "https://anaconda.org/anaconda/msys2-libidn2-devel", "description": "Implementation of the Stringprep, Punycode and IDNA specifications (repack of MSYS2-packages libidn2-devel for MSYS)" }, + { "name": "msys2-libintl", "uri": "https://anaconda.org/anaconda/msys2-libintl", "description": "GNU internationalization library (repack of MSYS2-packages libintl for MSYS)" }, + { "name": "msys2-libltdl", "uri": "https://anaconda.org/anaconda/msys2-libltdl", "description": "A generic library support script (repack of MSYS2-packages libltdl for MSYS)" }, + { "name": "msys2-liblzma", "uri": "https://anaconda.org/anaconda/msys2-liblzma", "description": "Library and command line tools for XZ and LZMA compressed files (repack of MSYS2-packages liblzma for MSYS)" }, + { "name": "msys2-liblzma-devel", "uri": "https://anaconda.org/anaconda/msys2-liblzma-devel", "description": "Library and command line tools for XZ and LZMA compressed files (repack of MSYS2-packages liblzma-devel for MSYS)" }, + { "name": "msys2-libnghttp2", "uri": "https://anaconda.org/anaconda/msys2-libnghttp2", "description": "Framing layer of HTTP/2 is implemented as a reusable C library (repack of MSYS2-packages libnghttp2 for MSYS)" }, + { "name": "msys2-libnghttp2-devel", "uri": "https://anaconda.org/anaconda/msys2-libnghttp2-devel", "description": "Framing layer of HTTP/2 is implemented as a reusable C library (repack of MSYS2-packages libnghttp2-devel for MSYS)" }, + { "name": "msys2-libopenssl", "uri": "https://anaconda.org/anaconda/msys2-libopenssl", "description": "The Open Source toolkit for Secure Sockets Layer and Transport Layer Security (repack of MSYS2-packages libopenssl for MSYS)" }, + { "name": "msys2-libp11-kit", "uri": "https://anaconda.org/anaconda/msys2-libp11-kit", "description": "Library to work with PKCS-11 modules (repack of MSYS2-packages libp11-kit for MSYS)" }, + { "name": "msys2-libp11-kit-devel", "uri": "https://anaconda.org/anaconda/msys2-libp11-kit-devel", "description": "Library to work with PKCS-11 modules (repack of MSYS2-packages libp11-kit-devel for MSYS)" }, + { "name": "msys2-libpcre", "uri": "https://anaconda.org/anaconda/msys2-libpcre", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre for MSYS)" }, + { "name": "msys2-libpcre16", "uri": "https://anaconda.org/anaconda/msys2-libpcre16", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre16 for MSYS)" }, + { "name": "msys2-libpcre2_16", "uri": "https://anaconda.org/anaconda/msys2-libpcre2_16", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre2_16 for MSYS)" }, + { "name": "msys2-libpcre2_32", "uri": "https://anaconda.org/anaconda/msys2-libpcre2_32", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre2_32 for MSYS)" }, + { "name": "msys2-libpcre2_8", "uri": "https://anaconda.org/anaconda/msys2-libpcre2_8", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre2_8 for MSYS)" }, + { "name": "msys2-libpcre2posix", "uri": "https://anaconda.org/anaconda/msys2-libpcre2posix", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre2posix for MSYS)" }, + { "name": "msys2-libpcre32", "uri": "https://anaconda.org/anaconda/msys2-libpcre32", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcre32 for MSYS)" }, + { "name": "msys2-libpcrecpp", "uri": "https://anaconda.org/anaconda/msys2-libpcrecpp", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcrecpp for MSYS)" }, + { "name": "msys2-libpcreposix", "uri": "https://anaconda.org/anaconda/msys2-libpcreposix", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages libpcreposix for MSYS)" }, + { "name": "msys2-libpsl", "uri": "https://anaconda.org/anaconda/msys2-libpsl", "description": "Public Suffix List library (repack of MSYS2-packages libpsl for MSYS)" }, + { "name": "msys2-libpsl-devel", "uri": "https://anaconda.org/anaconda/msys2-libpsl-devel", "description": "Public Suffix List library (repack of MSYS2-packages libpsl-devel for MSYS)" }, + { "name": "msys2-libreadline", "uri": "https://anaconda.org/anaconda/msys2-libreadline", "description": "GNU readline library (repack of MSYS2-packages libreadline for MSYS)" }, + { "name": "msys2-libreadline-devel", "uri": "https://anaconda.org/anaconda/msys2-libreadline-devel", "description": "GNU readline library (repack of MSYS2-packages libreadline-devel for MSYS)" }, + { "name": "msys2-libsqlite", "uri": "https://anaconda.org/anaconda/msys2-libsqlite", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages libsqlite for MSYS)" }, + { "name": "msys2-libsqlite-devel", "uri": "https://anaconda.org/anaconda/msys2-libsqlite-devel", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages libsqlite-devel for MSYS)" }, + { "name": "msys2-libssh2", "uri": "https://anaconda.org/anaconda/msys2-libssh2", "description": "A library implementing the SSH2 protocol as defined by Internet Drafts (repack of MSYS2-packages libssh2 for MSYS)" }, + { "name": "msys2-libssh2-devel", "uri": "https://anaconda.org/anaconda/msys2-libssh2-devel", "description": "A library implementing the SSH2 protocol as defined by Internet Drafts (repack of MSYS2-packages libssh2-devel for MSYS)" }, + { "name": "msys2-libtasn1", "uri": "https://anaconda.org/anaconda/msys2-libtasn1", "description": "A library for Abstract Syntax Notation One (ASN.1) and Distinguish Encoding Rules (DER) manipulation (repack of MSYS2-packages libtasn1 for MSYS)" }, + { "name": "msys2-libtasn1-devel", "uri": "https://anaconda.org/anaconda/msys2-libtasn1-devel", "description": "A library for Abstract Syntax Notation One (ASN.1) and Distinguish Encoding Rules (DER) manipulation (repack of MSYS2-packages libtasn1-devel for MSYS)" }, + { "name": "msys2-libtool", "uri": "https://anaconda.org/anaconda/msys2-libtool", "description": "A generic library support script (repack of MSYS2-packages libtool for MSYS)" }, + { "name": "msys2-libunistring", "uri": "https://anaconda.org/anaconda/msys2-libunistring", "description": "Library for manipulating Unicode strings and C strings. (repack of MSYS2-packages libunistring for MSYS)" }, + { "name": "msys2-libunistring-devel", "uri": "https://anaconda.org/anaconda/msys2-libunistring-devel", "description": "Library for manipulating Unicode strings and C strings. (repack of MSYS2-packages libunistring-devel for MSYS)" }, + { "name": "msys2-libutil-linux", "uri": "https://anaconda.org/anaconda/msys2-libutil-linux", "description": "Miscellaneous system utilities for Linux (repack of MSYS2-packages libutil-linux for MSYS)" }, + { "name": "msys2-libutil-linux-devel", "uri": "https://anaconda.org/anaconda/msys2-libutil-linux-devel", "description": "Miscellaneous system utilities for Linux (repack of MSYS2-packages libutil-linux-devel for MSYS)" }, + { "name": "msys2-libxcrypt", "uri": "https://anaconda.org/anaconda/msys2-libxcrypt", "description": "Modern library for one-way hashing of passwords (repack of MSYS2-packages libxcrypt for MSYS)" }, + { "name": "msys2-libxcrypt-devel", "uri": "https://anaconda.org/anaconda/msys2-libxcrypt-devel", "description": "Modern library for one-way hashing of passwords (repack of MSYS2-packages libxcrypt-devel for MSYS)" }, + { "name": "msys2-libxml2", "uri": "https://anaconda.org/anaconda/msys2-libxml2", "description": "XML parsing library, version 2 (repack of MSYS2-packages libxml2 for MSYS)" }, + { "name": "msys2-libxml2-devel", "uri": "https://anaconda.org/anaconda/msys2-libxml2-devel", "description": "XML parsing library, version 2 (repack of MSYS2-packages libxml2-devel for MSYS)" }, + { "name": "msys2-libzstd", "uri": "https://anaconda.org/anaconda/msys2-libzstd", "description": "Zstandard - Fast real-time compression algorithm (repack of MSYS2-packages libzstd for MSYS)" }, + { "name": "msys2-libzstd-devel", "uri": "https://anaconda.org/anaconda/msys2-libzstd-devel", "description": "Zstandard - Fast real-time compression algorithm (repack of MSYS2-packages libzstd-devel for MSYS)" }, + { "name": "msys2-m4", "uri": "https://anaconda.org/anaconda/msys2-m4", "description": "The GNU macro processor (repack of MSYS2-packages m4 for MSYS)" }, + { "name": "msys2-make", "uri": "https://anaconda.org/anaconda/msys2-make", "description": "GNU make utility to maintain groups of programs (repack of MSYS2-packages make for MSYS)" }, + { "name": "msys2-mingw-w64-mutex", "uri": "https://anaconda.org/anaconda/msys2-mingw-w64-mutex", "description": "A mutex package to ensure environment exclusivity between MSYS2 mingw-w64 environments" }, + { "name": "msys2-mintty", "uri": "https://anaconda.org/anaconda/msys2-mintty", "description": "Terminal emulator with native Windows look and feel (repack of MSYS2-packages mintty for MSYS)" }, + { "name": "msys2-mpc", "uri": "https://anaconda.org/anaconda/msys2-mpc", "description": "Multiple precision complex arithmetic library (repack of MSYS2-packages mpc for MSYS)" }, + { "name": "msys2-mpc-devel", "uri": "https://anaconda.org/anaconda/msys2-mpc-devel", "description": "Multiple precision complex arithmetic library (repack of MSYS2-packages mpc-devel for MSYS)" }, + { "name": "msys2-mpfr", "uri": "https://anaconda.org/anaconda/msys2-mpfr", "description": "Multiple-precision floating-point library (repack of MSYS2-packages mpfr for MSYS)" }, + { "name": "msys2-mpfr-devel", "uri": "https://anaconda.org/anaconda/msys2-mpfr-devel", "description": "Multiple-precision floating-point library (repack of MSYS2-packages mpfr-devel for MSYS)" }, + { "name": "msys2-msys-mutex", "uri": "https://anaconda.org/anaconda/msys2-msys-mutex", "description": "A mutex package to ensure environment exclusivity between MSYS2 MSYS environments" }, + { "name": "msys2-msys2-launcher", "uri": "https://anaconda.org/anaconda/msys2-msys2-launcher", "description": "Helper for launching MSYS2 shells (repack of MSYS2-packages msys2-launcher for MSYS)" }, + { "name": "msys2-msys2-runtime", "uri": "https://anaconda.org/anaconda/msys2-msys2-runtime", "description": "Cygwin POSIX emulation engine (repack of MSYS2-packages msys2-runtime for MSYS)" }, + { "name": "msys2-msys2-runtime-devel", "uri": "https://anaconda.org/anaconda/msys2-msys2-runtime-devel", "description": "Cygwin POSIX emulation engine (repack of MSYS2-packages msys2-runtime-devel for MSYS)" }, + { "name": "msys2-msys2-w32api-headers", "uri": "https://anaconda.org/anaconda/msys2-msys2-w32api-headers", "description": "Win32 API headers for MSYS2 32bit toolchain (repack of MSYS2-packages msys2-w32api-headers for MSYS)" }, + { "name": "msys2-msys2-w32api-runtime", "uri": "https://anaconda.org/anaconda/msys2-msys2-w32api-runtime", "description": "Win32 API import libs for MSYS2 toolchain (repack of MSYS2-packages msys2-w32api-runtime for MSYS)" }, + { "name": "msys2-nano", "uri": "https://anaconda.org/anaconda/msys2-nano", "description": "Pico editor clone with enhancements (repack of MSYS2-packages nano for MSYS)" }, + { "name": "msys2-ncurses", "uri": "https://anaconda.org/anaconda/msys2-ncurses", "description": "System V Release 4.0 curses emulation library (repack of MSYS2-packages ncurses for MSYS)" }, + { "name": "msys2-ncurses-devel", "uri": "https://anaconda.org/anaconda/msys2-ncurses-devel", "description": "System V Release 4.0 curses emulation library (repack of MSYS2-packages ncurses-devel for MSYS)" }, + { "name": "msys2-nghttp2", "uri": "https://anaconda.org/anaconda/msys2-nghttp2", "description": "Framing layer of HTTP/2 is implemented as a reusable C library (repack of MSYS2-packages nghttp2 for MSYS)" }, + { "name": "msys2-openssh", "uri": "https://anaconda.org/anaconda/msys2-openssh", "description": "Free version of the SSH connectivity tools (repack of MSYS2-packages openssh for MSYS)" }, + { "name": "msys2-openssl", "uri": "https://anaconda.org/anaconda/msys2-openssl", "description": "The Open Source toolkit for Secure Sockets Layer and Transport Layer Security (repack of MSYS2-packages openssl for MSYS)" }, + { "name": "msys2-openssl-devel", "uri": "https://anaconda.org/anaconda/msys2-openssl-devel", "description": "The Open Source toolkit for Secure Sockets Layer and Transport Layer Security (repack of MSYS2-packages openssl-devel for MSYS)" }, + { "name": "msys2-openssl-docs", "uri": "https://anaconda.org/anaconda/msys2-openssl-docs", "description": "The Open Source toolkit for Secure Sockets Layer and Transport Layer Security (repack of MSYS2-packages openssl-docs for MSYS)" }, + { "name": "msys2-p11-kit", "uri": "https://anaconda.org/anaconda/msys2-p11-kit", "description": "Library to work with PKCS-11 modules (repack of MSYS2-packages p11-kit for MSYS)" }, + { "name": "msys2-p7zip", "uri": "https://anaconda.org/anaconda/msys2-p7zip", "description": "Command-line version of the 7zip compressed file archiver (repack of MSYS2-packages p7zip for MSYS)" }, + { "name": "msys2-patch", "uri": "https://anaconda.org/anaconda/msys2-patch", "description": "A utility to apply patch files to original sources (repack of MSYS2-packages patch for MSYS)" }, + { "name": "msys2-pcre", "uri": "https://anaconda.org/anaconda/msys2-pcre", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages pcre for MSYS)" }, + { "name": "msys2-pcre-devel", "uri": "https://anaconda.org/anaconda/msys2-pcre-devel", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages pcre-devel for MSYS)" }, + { "name": "msys2-pcre2", "uri": "https://anaconda.org/anaconda/msys2-pcre2", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages pcre2 for MSYS)" }, + { "name": "msys2-pcre2-devel", "uri": "https://anaconda.org/anaconda/msys2-pcre2-devel", "description": "A library that implements Perl 5-style regular expressions (repack of MSYS2-packages pcre2-devel for MSYS)" }, + { "name": "msys2-perl", "uri": "https://anaconda.org/anaconda/msys2-perl", "description": "A highly capable, feature-rich programming language (repack of MSYS2-packages perl for MSYS)" }, + { "name": "msys2-perl-authen-sasl", "uri": "https://anaconda.org/anaconda/msys2-perl-authen-sasl", "description": "Perl/CPAN Module Authen::SASL : SASL authentication framework (repack of MSYS2-packages perl-Authen-SASL for MSYS)" }, + { "name": "msys2-perl-clone", "uri": "https://anaconda.org/anaconda/msys2-perl-clone", "description": "Recursive copy of nested objects. (repack of MSYS2-packages perl-Clone for MSYS)" }, + { "name": "msys2-perl-convert-binhex", "uri": "https://anaconda.org/anaconda/msys2-perl-convert-binhex", "description": "Perl module to extract data from Macintosh BinHex files (repack of MSYS2-packages perl-Convert-BinHex for MSYS)" }, + { "name": "msys2-perl-devel", "uri": "https://anaconda.org/anaconda/msys2-perl-devel", "description": "A highly capable, feature-rich programming language (repack of MSYS2-packages perl-devel for MSYS)" }, + { "name": "msys2-perl-doc", "uri": "https://anaconda.org/anaconda/msys2-perl-doc", "description": "A highly capable, feature-rich programming language (repack of MSYS2-packages perl-doc for MSYS)" }, + { "name": "msys2-perl-encode-locale", "uri": "https://anaconda.org/anaconda/msys2-perl-encode-locale", "description": "Determine the locale encoding (repack of MSYS2-packages perl-Encode-Locale for MSYS)" }, + { "name": "msys2-perl-error", "uri": "https://anaconda.org/anaconda/msys2-perl-error", "description": "Perl/CPAN Error module - Error/exception handling in an OO-ish way (repack of MSYS2-packages perl-Error for MSYS)" }, + { "name": "msys2-perl-file-listing", "uri": "https://anaconda.org/anaconda/msys2-perl-file-listing", "description": "parse directory listing (repack of MSYS2-packages perl-File-Listing for MSYS)" }, + { "name": "msys2-perl-html-parser", "uri": "https://anaconda.org/anaconda/msys2-perl-html-parser", "description": "Perl HTML parser class (repack of MSYS2-packages perl-HTML-Parser for MSYS)" }, + { "name": "msys2-perl-html-tagset", "uri": "https://anaconda.org/anaconda/msys2-perl-html-tagset", "description": "Data tables useful in parsing HTML (repack of MSYS2-packages perl-HTML-Tagset for MSYS)" }, + { "name": "msys2-perl-http-cookiejar", "uri": "https://anaconda.org/anaconda/msys2-perl-http-cookiejar", "description": "A minimalist HTTP user agent cookie jar (repack of MSYS2-packages perl-http-cookiejar for MSYS)" }, + { "name": "msys2-perl-http-cookies", "uri": "https://anaconda.org/anaconda/msys2-perl-http-cookies", "description": "HTTP cookie jars (repack of MSYS2-packages perl-HTTP-Cookies for MSYS)" }, + { "name": "msys2-perl-http-daemon", "uri": "https://anaconda.org/anaconda/msys2-perl-http-daemon", "description": "A simple http server class (repack of MSYS2-packages perl-HTTP-Daemon for MSYS)" }, + { "name": "msys2-perl-http-date", "uri": "https://anaconda.org/anaconda/msys2-perl-http-date", "description": "Date conversion routines (repack of MSYS2-packages perl-HTTP-Date for MSYS)" }, + { "name": "msys2-perl-http-message", "uri": "https://anaconda.org/anaconda/msys2-perl-http-message", "description": "HTTP style messages (repack of MSYS2-packages perl-HTTP-Message for MSYS)" }, + { "name": "msys2-perl-http-negotiate", "uri": "https://anaconda.org/anaconda/msys2-perl-http-negotiate", "description": "choose a variant to serve (repack of MSYS2-packages perl-HTTP-Negotiate for MSYS)" }, + { "name": "msys2-perl-io-html", "uri": "https://anaconda.org/anaconda/msys2-perl-io-html", "description": "Open an HTML file with automatic charset detection (repack of MSYS2-packages perl-IO-HTML for MSYS)" }, + { "name": "msys2-perl-io-socket-ssl", "uri": "https://anaconda.org/anaconda/msys2-perl-io-socket-ssl", "description": "Nearly transparent SSL encapsulation for IO::Socket::INET (repack of MSYS2-packages perl-IO-Socket-SSL for MSYS)" }, + { "name": "msys2-perl-io-stringy", "uri": "https://anaconda.org/anaconda/msys2-perl-io-stringy", "description": "I/O on in-core objects like strings/arrays (repack of MSYS2-packages perl-IO-Stringy for MSYS)" }, + { "name": "msys2-perl-libwww", "uri": "https://anaconda.org/anaconda/msys2-perl-libwww", "description": "The World-Wide Web library for Perl (repack of MSYS2-packages perl-libwww for MSYS)" }, + { "name": "msys2-perl-lwp-mediatypes", "uri": "https://anaconda.org/anaconda/msys2-perl-lwp-mediatypes", "description": "Guess the media type of a file or a URL (repack of MSYS2-packages perl-LWP-MediaTypes for MSYS)" }, + { "name": "msys2-perl-mailtools", "uri": "https://anaconda.org/anaconda/msys2-perl-mailtools", "description": "Various e-mail related modules (repack of MSYS2-packages perl-MailTools for MSYS)" }, + { "name": "msys2-perl-mime-tools", "uri": "https://anaconda.org/anaconda/msys2-perl-mime-tools", "description": "Parses streams to create MIME entities (repack of MSYS2-packages perl-MIME-tools for MSYS)" }, + { "name": "msys2-perl-net-http", "uri": "https://anaconda.org/anaconda/msys2-perl-net-http", "description": "Low-level HTTP connection (client) (repack of MSYS2-packages perl-Net-HTTP for MSYS)" }, + { "name": "msys2-perl-net-smtp-ssl", "uri": "https://anaconda.org/anaconda/msys2-perl-net-smtp-ssl", "description": "SSL support for Net::SMTP (repack of MSYS2-packages perl-Net-SMTP-SSL for MSYS)" }, + { "name": "msys2-perl-net-ssleay", "uri": "https://anaconda.org/anaconda/msys2-perl-net-ssleay", "description": "Perl extension for using OpenSSL (repack of MSYS2-packages perl-Net-SSLeay for MSYS)" }, + { "name": "msys2-perl-termreadkey", "uri": "https://anaconda.org/anaconda/msys2-perl-termreadkey", "description": "Provides simple control over terminal driver modes (repack of MSYS2-packages perl-TermReadKey for MSYS)" }, + { "name": "msys2-perl-timedate", "uri": "https://anaconda.org/anaconda/msys2-perl-timedate", "description": "Date formating subroutines (repack of MSYS2-packages perl-TimeDate for MSYS)" }, + { "name": "msys2-perl-try-tiny", "uri": "https://anaconda.org/anaconda/msys2-perl-try-tiny", "description": "Minimal try/catch with proper localization of $@ (repack of MSYS2-packages perl-Try-Tiny for MSYS)" }, + { "name": "msys2-perl-uri", "uri": "https://anaconda.org/anaconda/msys2-perl-uri", "description": "Uniform Resource Identifiers (absolute and relative) (repack of MSYS2-packages perl-URI for MSYS)" }, + { "name": "msys2-perl-www-robotrules", "uri": "https://anaconda.org/anaconda/msys2-perl-www-robotrules", "description": "Database of robots.txt-derived permissions (repack of MSYS2-packages perl-WWW-RobotRules for MSYS)" }, + { "name": "msys2-pkg-config", "uri": "https://anaconda.org/anaconda/msys2-pkg-config", "description": "msys2-pkgconf wrapper to align with non-MSYS2 systems" }, + { "name": "msys2-pkgconf", "uri": "https://anaconda.org/anaconda/msys2-pkgconf", "description": "pkg-config compatible utility which does not depend on glib (repack of MSYS2-packages pkgconf for MSYS)" }, + { "name": "msys2-posix", "uri": "https://anaconda.org/anaconda/msys2-posix", "description": "MSYS POSIX development packages" }, + { "name": "msys2-sed", "uri": "https://anaconda.org/anaconda/msys2-sed", "description": "GNU stream editor (repack of MSYS2-packages sed for MSYS)" }, + { "name": "msys2-sqlite", "uri": "https://anaconda.org/anaconda/msys2-sqlite", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages sqlite for MSYS)" }, + { "name": "msys2-sqlite-doc", "uri": "https://anaconda.org/anaconda/msys2-sqlite-doc", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages sqlite-doc for MSYS)" }, + { "name": "msys2-sqlite-extensions", "uri": "https://anaconda.org/anaconda/msys2-sqlite-extensions", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages sqlite-extensions for MSYS)" }, + { "name": "msys2-tar", "uri": "https://anaconda.org/anaconda/msys2-tar", "description": "Utility used to store, backup, and transport files (repack of MSYS2-packages tar for MSYS)" }, + { "name": "msys2-tcl", "uri": "https://anaconda.org/anaconda/msys2-tcl", "description": "The Tcl scripting language (repack of MSYS2-packages tcl for MSYS)" }, + { "name": "msys2-tcl-devel", "uri": "https://anaconda.org/anaconda/msys2-tcl-devel", "description": "The Tcl scripting language (repack of MSYS2-packages tcl-devel for MSYS)" }, + { "name": "msys2-tcl-doc", "uri": "https://anaconda.org/anaconda/msys2-tcl-doc", "description": "The Tcl scripting language (repack of MSYS2-packages tcl-doc for MSYS)" }, + { "name": "msys2-tcl-sqlite", "uri": "https://anaconda.org/anaconda/msys2-tcl-sqlite", "description": "A C library that implements an SQL database engine (repack of MSYS2-packages tcl-sqlite for MSYS)" }, + { "name": "msys2-texinfo", "uri": "https://anaconda.org/anaconda/msys2-texinfo", "description": "Utilities to work with and produce manuals, ASCII text, and on-line documentation from a single source file (repack of MSYS2-packages texinfo for MSYS)" }, + { "name": "msys2-texinfo-tex", "uri": "https://anaconda.org/anaconda/msys2-texinfo-tex", "description": "Utilities to work with and produce manuals, ASCII text, and on-line documentation from a single source file (repack of MSYS2-packages texinfo-tex for MSYS)" }, + { "name": "msys2-time", "uri": "https://anaconda.org/anaconda/msys2-time", "description": "Utility for monitoring a program's use of system resources (repack of MSYS2-packages time for MSYS)" }, + { "name": "msys2-tzcode", "uri": "https://anaconda.org/anaconda/msys2-tzcode", "description": "Sources for time zone and daylight saving time data (repack of MSYS2-packages tzcode for MSYS)" }, + { "name": "msys2-unzip", "uri": "https://anaconda.org/anaconda/msys2-unzip", "description": "Unpacks .zip archives such as those made by PKZIP (repack of MSYS2-packages unzip for MSYS)" }, + { "name": "msys2-util-linux", "uri": "https://anaconda.org/anaconda/msys2-util-linux", "description": "Miscellaneous system utilities for Linux (repack of MSYS2-packages util-linux for MSYS)" }, + { "name": "msys2-which", "uri": "https://anaconda.org/anaconda/msys2-which", "description": "A utility to show the full path of commands (repack of MSYS2-packages which for MSYS)" }, + { "name": "msys2-windows-default-manifest", "uri": "https://anaconda.org/anaconda/msys2-windows-default-manifest", "description": "Default Windows application manifest (repack of MSYS2-packages windows-default-manifest for MSYS)" }, + { "name": "msys2-xz", "uri": "https://anaconda.org/anaconda/msys2-xz", "description": "Library and command line tools for XZ and LZMA compressed files (repack of MSYS2-packages xz for MSYS)" }, + { "name": "msys2-zip", "uri": "https://anaconda.org/anaconda/msys2-zip", "description": "Creates PKZIP-compatible .zip files (repack of MSYS2-packages zip for MSYS)" }, + { "name": "msys2-zlib", "uri": "https://anaconda.org/anaconda/msys2-zlib", "description": "Compression library implementing the deflate compression method found in gzip and PKZIP (repack of MSYS2-packages zlib for MSYS)" }, + { "name": "msys2-zlib-devel", "uri": "https://anaconda.org/anaconda/msys2-zlib-devel", "description": "Compression library implementing the deflate compression method found in gzip and PKZIP (repack of MSYS2-packages zlib-devel for MSYS)" }, + { "name": "msys2-zstd", "uri": "https://anaconda.org/anaconda/msys2-zstd", "description": "Zstandard - Fast real-time compression algorithm (repack of MSYS2-packages zstd for MSYS)" }, + { "name": "multi_key_dict", "uri": "https://anaconda.org/anaconda/multi_key_dict", "description": "Multi key dictionary implementation" }, + { "name": "multidict", "uri": "https://anaconda.org/anaconda/multidict", "description": "The multidict implementation" }, + { "name": "multimethod", "uri": "https://anaconda.org/anaconda/multimethod", "description": "Multiple argument dispatching." }, + { "name": "multipart", "uri": "https://anaconda.org/anaconda/multipart", "description": "Parser for multipart/form-data.#" }, + { "name": "multipledispatch", "uri": "https://anaconda.org/anaconda/multipledispatch", "description": "Multiple dispatch in Python" }, + { "name": "multiprocess", "uri": "https://anaconda.org/anaconda/multiprocess", "description": "better multiprocessing and multithreading in python" }, + { "name": "multivolumefile", "uri": "https://anaconda.org/anaconda/multivolumefile", "description": "multi volume file wrapper library" }, + { "name": "munkres", "uri": "https://anaconda.org/anaconda/munkres", "description": "The Munkres module provides an O(n^3) implementation of the Munkres algorithm (also called the Hungarian algorithm or the Kuhn-Munkres algorithm)." }, + { "name": "murmurhash", "uri": "https://anaconda.org/anaconda/murmurhash", "description": "Cython bindings for MurmurHash2" }, + { "name": "mxnet", "uri": "https://anaconda.org/anaconda/mxnet", "description": "MXNet metapackage for installing lib,py-MXNet Conda packages" }, + { "name": "mxnet-gpu", "uri": "https://anaconda.org/anaconda/mxnet-gpu", "description": "MXNet metapackage which pins a variant of MXNet(GPU) Conda package" }, + { "name": "mxnet-gpu_mkl", "uri": "https://anaconda.org/anaconda/mxnet-gpu_mkl", "description": "MXNet metapackage which pins a variant of MXNet Conda package" }, + { "name": "mxnet-gpu_openblas", "uri": "https://anaconda.org/anaconda/mxnet-gpu_openblas", "description": "MXNet metapackage which pins a variant of MXNet Conda package" }, + { "name": "mxnet-mkl", "uri": "https://anaconda.org/anaconda/mxnet-mkl", "description": "MXNet metapackage which pins a variant of MXNet Conda package" }, + { "name": "mxnet-openblas", "uri": "https://anaconda.org/anaconda/mxnet-openblas", "description": "MXNet metapackage which pins a variant of MXNet Conda package" }, + { "name": "mypy", "uri": "https://anaconda.org/anaconda/mypy", "description": "Optional static typing for Python" }, + { "name": "mypy-protobuf", "uri": "https://anaconda.org/anaconda/mypy-protobuf", "description": "Generate mypy stub files from protobuf specs" }, + { "name": "mypy_extensions", "uri": "https://anaconda.org/anaconda/mypy_extensions", "description": "Extensions for mypy" }, + { "name": "mypyc", "uri": "https://anaconda.org/anaconda/mypyc", "description": "Optional static typing for Python" }, + { "name": "mysql", "uri": "https://anaconda.org/anaconda/mysql", "description": "Open source relational database management system." }, + { "name": "mysql-connector-c", "uri": "https://anaconda.org/anaconda/mysql-connector-c", "description": "MySQL Connector/C, the C interface for communicating with MySQL servers." }, + { "name": "mysql-connector-python", "uri": "https://anaconda.org/anaconda/mysql-connector-python", "description": "Python driver for communicating with MySQL servers" }, + { "name": "mysqlclient", "uri": "https://anaconda.org/anaconda/mysqlclient", "description": "Python interface to MySQL" }, + { "name": "namex", "uri": "https://anaconda.org/anaconda/namex", "description": "Clean up the public namespace of your package" }, + { "name": "navigator-updater", "uri": "https://anaconda.org/anaconda/navigator-updater", "description": "Anaconda Navigator Updater" }, + { "name": "nb_conda", "uri": "https://anaconda.org/anaconda/nb_conda", "description": "Conda environment and package access extension from within Jupyter" }, + { "name": "nb_conda_kernels", "uri": "https://anaconda.org/anaconda/nb_conda_kernels", "description": "Launch Jupyter kernels for any installed conda environment" }, + { "name": "nbclassic", "uri": "https://anaconda.org/anaconda/nbclassic", "description": "Jupyter Notebook as a Jupyter Server Extension." }, + { "name": "nbclient", "uri": "https://anaconda.org/anaconda/nbclient", "description": "A client library for executing notebooks. Formally nbconvert's ExecutePreprocessor." }, + { "name": "nbconvert", "uri": "https://anaconda.org/anaconda/nbconvert", "description": "Converting Jupyter Notebooks" }, + { "name": "nbconvert-all", "uri": "https://anaconda.org/anaconda/nbconvert-all", "description": "No Summary" }, + { "name": "nbconvert-core", "uri": "https://anaconda.org/anaconda/nbconvert-core", "description": "No Summary" }, + { "name": "nbconvert-pandoc", "uri": "https://anaconda.org/anaconda/nbconvert-pandoc", "description": "No Summary" }, + { "name": "nbdime", "uri": "https://anaconda.org/anaconda/nbdime", "description": "Diff and merge of Jupyter Notebooks" }, + { "name": "nbformat", "uri": "https://anaconda.org/anaconda/nbformat", "description": "The Jupyter Notebook format" }, + { "name": "nbgrader", "uri": "https://anaconda.org/anaconda/nbgrader", "description": "A system for assigning and grading Jupyter notebooks" }, + { "name": "nbserverproxy", "uri": "https://anaconda.org/anaconda/nbserverproxy", "description": "Jupyter server extension to proxy web services" }, + { "name": "nbsmoke", "uri": "https://anaconda.org/anaconda/nbsmoke", "description": "Basic notebook checks. Do they run? Do they contain lint?" }, + { "name": "nccl", "uri": "https://anaconda.org/anaconda/nccl", "description": "Optimized primitives for collective multi-GPU communication" }, + { "name": "ncurses-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/ncurses-amzn2-aarch64", "description": "(CDT) Ncurses support utilities" }, + { "name": "ncurses-base-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/ncurses-base-amzn2-aarch64", "description": "(CDT) Descriptions of common terminals" }, + { "name": "ncurses-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/ncurses-libs-amzn2-aarch64", "description": "(CDT) Ncurses libraries" }, + { "name": "neo4j-python-driver", "uri": "https://anaconda.org/anaconda/neo4j-python-driver", "description": "Database connector for Neo4j graph database" }, + { "name": "neon", "uri": "https://anaconda.org/anaconda/neon", "description": "Nervana's Python-based Deep Learning framework" }, + { "name": "neotime", "uri": "https://anaconda.org/anaconda/neotime", "description": "Nanosecond resolution temporal types" }, + { "name": "nest-asyncio", "uri": "https://anaconda.org/anaconda/nest-asyncio", "description": "Patch asyncio to allow nested event loops" }, + { "name": "netcdf4", "uri": "https://anaconda.org/anaconda/netcdf4", "description": "netcdf4-python is a Python interface to the netCDF C library." }, + { "name": "netifaces", "uri": "https://anaconda.org/anaconda/netifaces", "description": "Portable network interface information." }, + { "name": "nettle", "uri": "https://anaconda.org/anaconda/nettle", "description": "Nettle is a low-level cryptographic library that is designed to fit easily in more or less any context" }, + { "name": "nettle-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/nettle-amzn2-aarch64", "description": "(CDT) A low-level cryptographic library" }, + { "name": "networkx", "uri": "https://anaconda.org/anaconda/networkx", "description": "Python package for creating and manipulating complex networks" }, + { "name": "neuralprophet", "uri": "https://anaconda.org/anaconda/neuralprophet", "description": "NeuralProphet is an easy to learn framework for interpretable time series forecasting" }, + { "name": "nginx", "uri": "https://anaconda.org/anaconda/nginx", "description": "Nginx is an HTTP and reverse proxy server" }, + { "name": "ninja", "uri": "https://anaconda.org/anaconda/ninja", "description": "A small build system with a focus on speed" }, + { "name": "ninja-base", "uri": "https://anaconda.org/anaconda/ninja-base", "description": "A small build system with a focus on speed" }, + { "name": "nitro", "uri": "https://anaconda.org/anaconda/nitro", "description": "A GIT Mirror of Nitro NITF project" }, + { "name": "nlohmann_json", "uri": "https://anaconda.org/anaconda/nlohmann_json", "description": "JSON for Modern C++" }, + { "name": "nlopt", "uri": "https://anaconda.org/anaconda/nlopt", "description": "nonlinear optimization library" }, + { "name": "nltk", "uri": "https://anaconda.org/anaconda/nltk", "description": "Natural Language Toolkit" }, + { "name": "nodeenv", "uri": "https://anaconda.org/anaconda/nodeenv", "description": "Node.js virtual environment builder" }, + { "name": "nodejs", "uri": "https://anaconda.org/anaconda/nodejs", "description": "Node.js is an open-source, cross-platform JavaScript runtime environment." }, + { "name": "nose-exclude", "uri": "https://anaconda.org/anaconda/nose-exclude", "description": "Exclude specific directories from nosetests runs." }, + { "name": "nose-parameterized", "uri": "https://anaconda.org/anaconda/nose-parameterized", "description": "Parameterized testing with any Python test framework" }, + { "name": "nose2", "uri": "https://anaconda.org/anaconda/nose2", "description": "nose2 is the next generation of nicer testing for Python" }, + { "name": "notebook", "uri": "https://anaconda.org/anaconda/notebook", "description": "A web-based notebook environment for interactive computing" }, + { "name": "notebook-shim", "uri": "https://anaconda.org/anaconda/notebook-shim", "description": "A shim layer for notebook traits and config" }, + { "name": "nsight-compute", "uri": "https://anaconda.org/anaconda/nsight-compute", "description": "NVIDIA Nsight Compute is an interactive kernel profiler for CUDA applications" }, + { "name": "nspr-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/nspr-cos7-ppc64le", "description": "(CDT) Netscape Portable Runtime" }, + { "name": "nspr-cos7-s390x", "uri": "https://anaconda.org/anaconda/nspr-cos7-s390x", "description": "(CDT) Netscape Portable Runtime" }, + { "name": "nss-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/nss-cos7-ppc64le", "description": "(CDT) Network Security Services" }, + { "name": "nss-cos7-s390x", "uri": "https://anaconda.org/anaconda/nss-cos7-s390x", "description": "(CDT) Network Security Services" }, + { "name": "nss-softokn-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/nss-softokn-cos7-ppc64le", "description": "(CDT) Network Security Services Softoken Module" }, + { "name": "nss-softokn-cos7-s390x", "uri": "https://anaconda.org/anaconda/nss-softokn-cos7-s390x", "description": "(CDT) Network Security Services Softoken Module" }, + { "name": "nss-softokn-freebl-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/nss-softokn-freebl-cos7-ppc64le", "description": "(CDT) Freebl library for the Network Security Services" }, + { "name": "nss-softokn-freebl-cos7-s390x", "uri": "https://anaconda.org/anaconda/nss-softokn-freebl-cos7-s390x", "description": "(CDT) Freebl library for the Network Security Services" }, + { "name": "nss-util-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/nss-util-cos7-ppc64le", "description": "(CDT) Network Security Services Utilities Library" }, + { "name": "nss-util-cos7-s390x", "uri": "https://anaconda.org/anaconda/nss-util-cos7-s390x", "description": "(CDT) Network Security Services Utilities Library" }, + { "name": "nsync", "uri": "https://anaconda.org/anaconda/nsync", "description": "nsync is a C library that exports various synchronization primitives, such as mutexes" }, + { "name": "ntlm-auth", "uri": "https://anaconda.org/anaconda/ntlm-auth", "description": "Calculates NTLM Authentication codes" }, + { "name": "numactl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/numactl-amzn2-aarch64", "description": "(CDT) The numactl" }, + { "name": "numactl-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/numactl-devel-amzn2-aarch64", "description": "(CDT) The numactl developer" }, + { "name": "numba", "uri": "https://anaconda.org/anaconda/numba", "description": "NumPy aware dynamic Python compiler using LLVM" }, + { "name": "numba-cuda", "uri": "https://anaconda.org/anaconda/numba-cuda", "description": "CUDA target for Numba" }, + { "name": "numba-dppy", "uri": "https://anaconda.org/anaconda/numba-dppy", "description": "Numba extension for Intel CPU and GPU backend" }, + { "name": "numcodecs", "uri": "https://anaconda.org/anaconda/numcodecs", "description": "A Python package providing buffer compression and transformation codecs for use in data storage and communication applications." }, + { "name": "numexpr", "uri": "https://anaconda.org/anaconda/numexpr", "description": "Fast numerical expression evaluator for NumPy" }, + { "name": "numpy", "uri": "https://anaconda.org/anaconda/numpy", "description": "Array processing for numbers, strings, records, and objects." }, + { "name": "numpy-base", "uri": "https://anaconda.org/anaconda/numpy-base", "description": "Array processing for numbers, strings, records, and objects." }, + { "name": "numpy-devel", "uri": "https://anaconda.org/anaconda/numpy-devel", "description": "Array processing for numbers, strings, records, and objects." }, + { "name": "numpydoc", "uri": "https://anaconda.org/anaconda/numpydoc", "description": "Sphinx extension to support docstrings in Numpy format" }, + { "name": "nvcc_linux-64", "uri": "https://anaconda.org/anaconda/nvcc_linux-64", "description": "A meta-package to enable the right nvcc." }, + { "name": "nvcc_win-64", "uri": "https://anaconda.org/anaconda/nvcc_win-64", "description": "A meta-package to enable the right nvcc." }, + { "name": "nvidia-gds", "uri": "https://anaconda.org/anaconda/nvidia-gds", "description": "GPU Direct Storage meta-package" }, + { "name": "nvidia-ml", "uri": "https://anaconda.org/anaconda/nvidia-ml", "description": "Provides a Python interface to GPU management and monitoring functions." }, + { "name": "nvidia-ml-py", "uri": "https://anaconda.org/anaconda/nvidia-ml-py", "description": "Python Bindings for the NVIDIA Management Library" }, + { "name": "oauthenticator", "uri": "https://anaconda.org/anaconda/oauthenticator", "description": "OAuth + JupyterHub Authenticator = OAuthenticator" }, + { "name": "oauthlib", "uri": "https://anaconda.org/anaconda/oauthlib", "description": "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" }, + { "name": "objconv", "uri": "https://anaconda.org/anaconda/objconv", "description": "Object file converter" }, + { "name": "ocl-icd", "uri": "https://anaconda.org/anaconda/ocl-icd", "description": "An OpenCL ICD Loader under an open-source license" }, + { "name": "odo", "uri": "https://anaconda.org/anaconda/odo", "description": "Shapeshifting for your data" }, + { "name": "olefile", "uri": "https://anaconda.org/anaconda/olefile", "description": "parse, read and write Microsoft OLE2 files" }, + { "name": "omniscidb", "uri": "https://anaconda.org/anaconda/omniscidb", "description": "The OmniSci database" }, + { "name": "omniscidb-common", "uri": "https://anaconda.org/anaconda/omniscidb-common", "description": "The OmniSci / HeavyDB database common files." }, + { "name": "omniscidbe", "uri": "https://anaconda.org/anaconda/omniscidbe", "description": "The OmniSci / HeavyDB database" }, + { "name": "oneccl-devel", "uri": "https://anaconda.org/anaconda/oneccl-devel", "description": "Intel® oneAPI Collective Communications Library 2021.3.0 for Linux*" }, + { "name": "oniguruma", "uri": "https://anaconda.org/anaconda/oniguruma", "description": "A regular expression library." }, + { "name": "onnx", "uri": "https://anaconda.org/anaconda/onnx", "description": "Open Neural Network Exchange library" }, + { "name": "onnxconverter-common", "uri": "https://anaconda.org/anaconda/onnxconverter-common", "description": "Common utilities for ONNX converters" }, + { "name": "onnxmltools", "uri": "https://anaconda.org/anaconda/onnxmltools", "description": "ONNXMLTools enables conversion of models to ONNX" }, + { "name": "onnxruntime", "uri": "https://anaconda.org/anaconda/onnxruntime", "description": "cross-platform, high performance ML inferencing and training accelerator" }, + { "name": "onnxruntime-novec", "uri": "https://anaconda.org/anaconda/onnxruntime-novec", "description": "cross-platform, high performance ML inferencing and training accelerator" }, + { "name": "openai", "uri": "https://anaconda.org/anaconda/openai", "description": "Python client library for the OpenAI API" }, + { "name": "openapi-pydantic", "uri": "https://anaconda.org/anaconda/openapi-pydantic", "description": "Pydantic OpenAPI schema implementation" }, + { "name": "openapi-schema-pydantic", "uri": "https://anaconda.org/anaconda/openapi-schema-pydantic", "description": "OpenAPI (v3) specification schema as pydantic class" }, + { "name": "openapi-schema-validator", "uri": "https://anaconda.org/anaconda/openapi-schema-validator", "description": "OpenAPI schema validation for Python" }, + { "name": "openapi-spec-validator", "uri": "https://anaconda.org/anaconda/openapi-spec-validator", "description": "OpenAPI 2.0 (aka Swagger) and OpenAPI 3 spec validator" }, + { "name": "openblas-devel", "uri": "https://anaconda.org/anaconda/openblas-devel", "description": "OpenBLAS headers and libraries for developing software that used OpenBLAS." }, + { "name": "opencensus", "uri": "https://anaconda.org/anaconda/opencensus", "description": "OpenCensus - A stats collection and distributed tracing framework" }, + { "name": "opencensus-context", "uri": "https://anaconda.org/anaconda/opencensus-context", "description": "The OpenCensus Runtime Context." }, + { "name": "opencensus-proto", "uri": "https://anaconda.org/anaconda/opencensus-proto", "description": "OpenCensus Proto - Language Independent Interface Types For OpenCensus" }, + { "name": "opencv", "uri": "https://anaconda.org/anaconda/opencv", "description": "Computer vision and machine learning software library." }, + { "name": "opencv-suite", "uri": "https://anaconda.org/anaconda/opencv-suite", "description": "Computer vision and machine learning software library." }, + { "name": "openh264", "uri": "https://anaconda.org/anaconda/openh264", "description": "OpenH264 is a codec library which supports H.264 encoding and decoding" }, + { "name": "openhmd", "uri": "https://anaconda.org/anaconda/openhmd", "description": "Free and Open Source API and drivers for immersive technology" }, + { "name": "openjpeg", "uri": "https://anaconda.org/anaconda/openjpeg", "description": "An open-source JPEG 2000 codec written in C." }, + { "name": "openldap", "uri": "https://anaconda.org/anaconda/openldap", "description": "OpenLDAP Software is an open source implementation of the Lightweight Directory Access Protocol." }, + { "name": "openml", "uri": "https://anaconda.org/anaconda/openml", "description": "Python API for OpenML" }, + { "name": "openmpi-mpicc", "uri": "https://anaconda.org/anaconda/openmpi-mpicc", "description": "An open source Message Passing Interface implementation." }, + { "name": "openmpi-mpicxx", "uri": "https://anaconda.org/anaconda/openmpi-mpicxx", "description": "An open source Message Passing Interface implementation." }, + { "name": "openmpi-mpifort", "uri": "https://anaconda.org/anaconda/openmpi-mpifort", "description": "An open source Message Passing Interface implementation." }, + { "name": "openpyxl", "uri": "https://anaconda.org/anaconda/openpyxl", "description": "A Python library to read/write Excel 2010 xlsx/xlsm files" }, + { "name": "openresty", "uri": "https://anaconda.org/anaconda/openresty", "description": "No Summary" }, + { "name": "openssl", "uri": "https://anaconda.org/anaconda/openssl", "description": "OpenSSL is an open-source implementation of the SSL and TLS protocols" }, + { "name": "openssl-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/openssl-devel-amzn2-aarch64", "description": "(CDT) Files for development of applications which will use OpenSSL" }, + { "name": "openssl-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/openssl-libs-amzn2-aarch64", "description": "(CDT) A general purpose cryptography library with TLS implementation" }, + { "name": "opentelemetry-api", "uri": "https://anaconda.org/anaconda/opentelemetry-api", "description": "OpenTelemetry Python / API" }, + { "name": "opentelemetry-distro", "uri": "https://anaconda.org/anaconda/opentelemetry-distro", "description": "OpenTelemetry Python Distro" }, + { "name": "opentelemetry-exporter-otlp", "uri": "https://anaconda.org/anaconda/opentelemetry-exporter-otlp", "description": "OpenTelemetry Collector Exporters" }, + { "name": "opentelemetry-exporter-otlp-proto-common", "uri": "https://anaconda.org/anaconda/opentelemetry-exporter-otlp-proto-common", "description": "OpenTelemetry Protobuf encoding" }, + { "name": "opentelemetry-exporter-otlp-proto-grpc", "uri": "https://anaconda.org/anaconda/opentelemetry-exporter-otlp-proto-grpc", "description": "OpenTelemetry Python / Protobuf over gRPC Exporter" }, + { "name": "opentelemetry-exporter-otlp-proto-http", "uri": "https://anaconda.org/anaconda/opentelemetry-exporter-otlp-proto-http", "description": "OpenTelemetry Collector Protobuf over HTTP Exporter" }, + { "name": "opentelemetry-instrumentation", "uri": "https://anaconda.org/anaconda/opentelemetry-instrumentation", "description": "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" }, + { "name": "opentelemetry-instrumentation-system-metrics", "uri": "https://anaconda.org/anaconda/opentelemetry-instrumentation-system-metrics", "description": "OpenTelemetry Python Instrumentation for System Metrics" }, + { "name": "opentelemetry-opentracing-shim", "uri": "https://anaconda.org/anaconda/opentelemetry-opentracing-shim", "description": "OpenTelemetry Python | OpenTracing Shim" }, + { "name": "opentelemetry-propagator-b3", "uri": "https://anaconda.org/anaconda/opentelemetry-propagator-b3", "description": "OpenTelemetry B3 Propagator" }, + { "name": "opentelemetry-propagator-jaeger", "uri": "https://anaconda.org/anaconda/opentelemetry-propagator-jaeger", "description": "OpenTelemetry Python | Jaeger Propagator" }, + { "name": "opentelemetry-proto", "uri": "https://anaconda.org/anaconda/opentelemetry-proto", "description": "OpenTelemetry Python / Proto" }, + { "name": "opentelemetry-sdk", "uri": "https://anaconda.org/anaconda/opentelemetry-sdk", "description": "OpenTelemetry Python / SDK" }, + { "name": "opentelemetry-semantic-conventions", "uri": "https://anaconda.org/anaconda/opentelemetry-semantic-conventions", "description": "OpenTelemetry Python | Semantic Conventions" }, + { "name": "opentracing", "uri": "https://anaconda.org/anaconda/opentracing", "description": "OpenTracing API for Python." }, + { "name": "opentracing_instrumentation", "uri": "https://anaconda.org/anaconda/opentracing_instrumentation", "description": "Tracing Instrumentation using OpenTracing API (http://opentracing.io)" }, + { "name": "opentsne", "uri": "https://anaconda.org/anaconda/opentsne", "description": "Extensible, parallel implementations of t-SNE" }, + { "name": "opt_einsum", "uri": "https://anaconda.org/anaconda/opt_einsum", "description": "Optimizing einsum functions in NumPy, Tensorflow, Dask, and more with contraction order optimization." }, + { "name": "optax", "uri": "https://anaconda.org/anaconda/optax", "description": "A gradient processing and optimisation library in JAX." }, + { "name": "optimum", "uri": "https://anaconda.org/anaconda/optimum", "description": "🤗 Optimum is an extension of 🤗 Transformers and Diffusers, providing a set of\noptimization tools enabling maximum efficiency to train and run models on targeted hardware,\nwhile keeping things easy to use." }, + { "name": "optional-lite", "uri": "https://anaconda.org/anaconda/optional-lite", "description": "A C++17-like optional, a nullable object for C++98, C++11 and later in a single-file header-only library" }, + { "name": "optree", "uri": "https://anaconda.org/anaconda/optree", "description": "Optimized PyTree Utilities" }, + { "name": "oracledb", "uri": "https://anaconda.org/anaconda/oracledb", "description": "Python interface to Oracle Database" }, + { "name": "orange-canvas-core", "uri": "https://anaconda.org/anaconda/orange-canvas-core", "description": "Core component of Orange Canvas" }, + { "name": "orange-widget-base", "uri": "https://anaconda.org/anaconda/orange-widget-base", "description": "Base Widget for Orange Canvas" }, + { "name": "orange3", "uri": "https://anaconda.org/anaconda/orange3", "description": "component-based data mining framework" }, + { "name": "orbit2-cos6-i686", "uri": "https://anaconda.org/anaconda/orbit2-cos6-i686", "description": "(CDT) A high-performance CORBA Object Request Broker" }, + { "name": "orbit2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/orbit2-cos6-x86_64", "description": "(CDT) A high-performance CORBA Object Request Broker" }, + { "name": "orbit2-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/orbit2-cos7-ppc64le", "description": "(CDT) A high-performance CORBA Object Request Broker" }, + { "name": "orbit2-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/orbit2-devel-cos6-x86_64", "description": "(CDT) Development libraries, header files and utilities for ORBit" }, + { "name": "orc", "uri": "https://anaconda.org/anaconda/orc", "description": "C++ libraries for Apache ORC" }, + { "name": "ordered-set", "uri": "https://anaconda.org/anaconda/ordered-set", "description": "A MutableSet that remembers its order, so that every entry has an index." }, + { "name": "orderedmultidict", "uri": "https://anaconda.org/anaconda/orderedmultidict", "description": "Ordered Multivalue Dictionary - omdict." }, + { "name": "orjson", "uri": "https://anaconda.org/anaconda/orjson", "description": "orjson is a fast, correct JSON library for Python." }, + { "name": "oscrypto", "uri": "https://anaconda.org/anaconda/oscrypto", "description": "Compiler-free Python crypto library backed by the OS, supporting CPython and PyPy" }, + { "name": "osqp", "uri": "https://anaconda.org/anaconda/osqp", "description": "Python interface for OSQP, the Operator Splitting QP Solver" }, + { "name": "outcome", "uri": "https://anaconda.org/anaconda/outcome", "description": "Capture the outcome of Python function calls." }, + { "name": "overrides", "uri": "https://anaconda.org/anaconda/overrides", "description": "A decorator to automatically detect mismatch when overriding a method" }, + { "name": "owslib", "uri": "https://anaconda.org/anaconda/owslib", "description": "OGC Web Service utility library" }, + { "name": "p11-kit-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/p11-kit-amzn2-aarch64", "description": "(CDT) Library for loading and sharing PKCS#11 modules" }, + { "name": "p11-kit-cos6-i686", "uri": "https://anaconda.org/anaconda/p11-kit-cos6-i686", "description": "(CDT) Library for loading and sharing PKCS#11 modules" }, + { "name": "p11-kit-cos6-x86_64", "uri": "https://anaconda.org/anaconda/p11-kit-cos6-x86_64", "description": "(CDT) Library for loading and sharing PKCS#11 modules" }, + { "name": "p11-kit-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/p11-kit-cos7-ppc64le", "description": "(CDT) Library for loading and sharing PKCS#11 modules" }, + { "name": "p11-kit-cos7-s390x", "uri": "https://anaconda.org/anaconda/p11-kit-cos7-s390x", "description": "(CDT) Library for loading and sharing PKCS#11 modules" }, + { "name": "p11-kit-trust-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/p11-kit-trust-amzn2-aarch64", "description": "(CDT) System trust module from p11-kit" }, + { "name": "p11-kit-trust-cos6-i686", "uri": "https://anaconda.org/anaconda/p11-kit-trust-cos6-i686", "description": "(CDT) System trust module from p11-kit" }, + { "name": "p11-kit-trust-cos6-x86_64", "uri": "https://anaconda.org/anaconda/p11-kit-trust-cos6-x86_64", "description": "(CDT) System trust module from p11-kit" }, + { "name": "p11-kit-trust-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/p11-kit-trust-cos7-ppc64le", "description": "(CDT) System trust module from p11-kit" }, + { "name": "p11-kit-trust-cos7-s390x", "uri": "https://anaconda.org/anaconda/p11-kit-trust-cos7-s390x", "description": "(CDT) System trust module from p11-kit" }, + { "name": "p7zip", "uri": "https://anaconda.org/anaconda/p7zip", "description": "p7zip is a port of the Windows programs 7z.exe and 7za.exe provided by 7-zip." }, + { "name": "packaging", "uri": "https://anaconda.org/anaconda/packaging", "description": "Core utilities for Python packages" }, + { "name": "palettable", "uri": "https://anaconda.org/anaconda/palettable", "description": "Color palettes for Python." }, + { "name": "pam-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pam-amzn2-aarch64", "description": "(CDT) An extensible library which provides authentication for applications" }, + { "name": "pam-cos6-i686", "uri": "https://anaconda.org/anaconda/pam-cos6-i686", "description": "(CDT) An extensible library which provides authentication for applications" }, + { "name": "pam-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pam-cos6-x86_64", "description": "(CDT) An extensible library which provides authentication for applications" }, + { "name": "pam-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/pam-devel-cos6-i686", "description": "(CDT) Files needed for developing PAM-aware applications and modules for PAM" }, + { "name": "pam-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pam-devel-cos6-x86_64", "description": "(CDT) Files needed for developing PAM-aware applications and modules for PAM" }, + { "name": "pamela", "uri": "https://anaconda.org/anaconda/pamela", "description": "PAM interface using ctypes" }, + { "name": "pandarallel", "uri": "https://anaconda.org/anaconda/pandarallel", "description": "An easy to use library to speed up computation (by parallelizing on multi CPUs) with pandas." }, + { "name": "pandas", "uri": "https://anaconda.org/anaconda/pandas", "description": "High-performance, easy-to-use data structures and data analysis tools." }, + { "name": "pandas-profiling", "uri": "https://anaconda.org/anaconda/pandas-profiling", "description": "Generate profile report for pandas DataFrame" }, + { "name": "pandas-stubs", "uri": "https://anaconda.org/anaconda/pandas-stubs", "description": "Collection of Pandas stub files" }, + { "name": "pandasql", "uri": "https://anaconda.org/anaconda/pandasql", "description": "No Summary" }, + { "name": "pandera-core", "uri": "https://anaconda.org/anaconda/pandera-core", "description": "The open source framework for precision data testing" }, + { "name": "pandoc", "uri": "https://anaconda.org/anaconda/pandoc", "description": "Universal markup converter (repackaged binaries)" }, + { "name": "panel", "uri": "https://anaconda.org/anaconda/panel", "description": "The powerful data exploration & web app framework for Python" }, + { "name": "pango", "uri": "https://anaconda.org/anaconda/pango", "description": "Text layout and rendering engine." }, + { "name": "pango-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pango-amzn2-aarch64", "description": "(CDT) System for layout and rendering of internationalized text" }, + { "name": "pango-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pango-cos6-x86_64", "description": "(CDT) System for layout and rendering of internationalized text" }, + { "name": "pango-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/pango-cos7-ppc64le", "description": "(CDT) System for layout and rendering of internationalized text" }, + { "name": "pango-cos7-s390x", "uri": "https://anaconda.org/anaconda/pango-cos7-s390x", "description": "(CDT) System for layout and rendering of internationalized text" }, + { "name": "pango-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pango-devel-amzn2-aarch64", "description": "(CDT) Development files for pango" }, + { "name": "pango-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/pango-devel-cos7-ppc64le", "description": "(CDT) Development files for pango" }, + { "name": "pango-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/pango-devel-cos7-s390x", "description": "(CDT) Development files for pango" }, + { "name": "pangomm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pangomm-amzn2-aarch64", "description": "(CDT) C++ interface for Pango" }, + { "name": "pangomm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pangomm-devel-amzn2-aarch64", "description": "(CDT) Headers for developing programs that will use pangomm" }, + { "name": "param", "uri": "https://anaconda.org/anaconda/param", "description": "Param: Make your Python code clearer and more reliable by declaring Parameters" }, + { "name": "parameterized", "uri": "https://anaconda.org/anaconda/parameterized", "description": "Parameterized testing with any Python test framework" }, + { "name": "paramiko", "uri": "https://anaconda.org/anaconda/paramiko", "description": "SSH2 protocol library" }, + { "name": "parquet-cpp", "uri": "https://anaconda.org/anaconda/parquet-cpp", "description": "C++ libraries for the Apache Parquet file format" }, + { "name": "parse", "uri": "https://anaconda.org/anaconda/parse", "description": "parse() is the opposite of format()" }, + { "name": "parse_type", "uri": "https://anaconda.org/anaconda/parse_type", "description": "Simplifies to build parse types based on the parse module" }, + { "name": "parsedatetime", "uri": "https://anaconda.org/anaconda/parsedatetime", "description": "Parse human-readable date/time text." }, + { "name": "parsel", "uri": "https://anaconda.org/anaconda/parsel", "description": "library to extract data from HTML and XML using XPath and CSS selectors" }, + { "name": "parso", "uri": "https://anaconda.org/anaconda/parso", "description": "A Python Parser" }, + { "name": "partd", "uri": "https://anaconda.org/anaconda/partd", "description": "Appendable key-value storage" }, + { "name": "pastel", "uri": "https://anaconda.org/anaconda/pastel", "description": "Bring colors to your terminal" }, + { "name": "patch-ng", "uri": "https://anaconda.org/anaconda/patch-ng", "description": "Library to parse and apply unified diffs" }, + { "name": "path", "uri": "https://anaconda.org/anaconda/path", "description": "A module wrapper for os.path" }, + { "name": "pathable", "uri": "https://anaconda.org/anaconda/pathable", "description": "Object-oriented paths" }, + { "name": "pathlib2", "uri": "https://anaconda.org/anaconda/pathlib2", "description": "Fork of pathlib aiming to support the full stdlib Python API" }, + { "name": "pathos", "uri": "https://anaconda.org/anaconda/pathos", "description": "parallel graph management and execution in heterogeneous computing" }, + { "name": "pathspec", "uri": "https://anaconda.org/anaconda/pathspec", "description": "Utility library for gitignore style pattern matching of file paths." }, + { "name": "pathtools", "uri": "https://anaconda.org/anaconda/pathtools", "description": "Path utilities for Python." }, + { "name": "pathy", "uri": "https://anaconda.org/anaconda/pathy", "description": "A Path interface for local and cloud bucket storage" }, + { "name": "patsy", "uri": "https://anaconda.org/anaconda/patsy", "description": "Describing statistical models in Python using symbolic formulas" }, + { "name": "pbkdf2", "uri": "https://anaconda.org/anaconda/pbkdf2", "description": "No Summary" }, + { "name": "pciutils-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pciutils-amzn2-aarch64", "description": "(CDT) PCI bus related utilities" }, + { "name": "pciutils-cos7-s390x", "uri": "https://anaconda.org/anaconda/pciutils-cos7-s390x", "description": "(CDT) PCI bus related utilities" }, + { "name": "pciutils-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pciutils-devel-amzn2-aarch64", "description": "(CDT) Linux PCI development library" }, + { "name": "pciutils-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/pciutils-devel-cos6-i686", "description": "(CDT) Linux PCI development library" }, + { "name": "pciutils-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/pciutils-devel-cos7-s390x", "description": "(CDT) Linux PCI development library" }, + { "name": "pciutils-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pciutils-libs-amzn2-aarch64", "description": "(CDT) Linux PCI library" }, + { "name": "pciutils-libs-cos7-s390x", "uri": "https://anaconda.org/anaconda/pciutils-libs-cos7-s390x", "description": "(CDT) Linux PCI library" }, + { "name": "pcre-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pcre-amzn2-aarch64", "description": "(CDT) Perl-compatible regular expression library" }, + { "name": "pcre2", "uri": "https://anaconda.org/anaconda/pcre2", "description": "Regular expression pattern matching using Perl 5 syntax and semantics." }, + { "name": "pdf2image", "uri": "https://anaconda.org/anaconda/pdf2image", "description": "A python module that wraps pdftoppm and pdftocairo to convert PDF to a PIL Image object" }, + { "name": "pdfium-binaries", "uri": "https://anaconda.org/anaconda/pdfium-binaries", "description": "pre-compiled binaries of the PDFium library" }, + { "name": "pdm", "uri": "https://anaconda.org/anaconda/pdm", "description": "Python Development Master" }, + { "name": "pdm-backend", "uri": "https://anaconda.org/anaconda/pdm-backend", "description": "The build backend used by PDM that supports latest packaging standards." }, + { "name": "pdm-pep517", "uri": "https://anaconda.org/anaconda/pdm-pep517", "description": "A PEP 517 backend for PDM that supports PEP 621 metadata" }, + { "name": "pdoc3", "uri": "https://anaconda.org/anaconda/pdoc3", "description": "Auto-generate API documentation for Python projects." }, + { "name": "pefile", "uri": "https://anaconda.org/anaconda/pefile", "description": "pefile is a Python module to read and work with PE (Portable Executable) files" }, + { "name": "pegen", "uri": "https://anaconda.org/anaconda/pegen", "description": "CPython's PEG parser generator" }, + { "name": "pendulum", "uri": "https://anaconda.org/anaconda/pendulum", "description": "Python datetimes made easy" }, + { "name": "pep517", "uri": "https://anaconda.org/anaconda/pep517", "description": "Wrappers to build Python packages using PEP 517 hooks" }, + { "name": "pep8-naming", "uri": "https://anaconda.org/anaconda/pep8-naming", "description": "Plug-in for flake 8 to check the PEP-8 naming conventions" }, + { "name": "percy", "uri": "https://anaconda.org/anaconda/percy", "description": "Helper tool for recipes on aggregate." }, + { "name": "perf", "uri": "https://anaconda.org/anaconda/perf", "description": "Python module to generate and modify perf" }, + { "name": "performance", "uri": "https://anaconda.org/anaconda/performance", "description": "Python benchmark suite" }, + { "name": "perl-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-amzn2-aarch64", "description": "(CDT) Practical Extraction and Report Language" }, + { "name": "perl-carp", "uri": "https://anaconda.org/anaconda/perl-carp", "description": "alternative warn and die for modules" }, + { "name": "perl-carp-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-carp-amzn2-aarch64", "description": "(CDT) Alternative warn and die for modules" }, + { "name": "perl-constant-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-constant-amzn2-aarch64", "description": "(CDT) Perl pragma to declare constants" }, + { "name": "perl-exporter-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-exporter-amzn2-aarch64", "description": "(CDT) Implements default import method for modules" }, + { "name": "perl-exporter-lite", "uri": "https://anaconda.org/anaconda/perl-exporter-lite", "description": "lightweight exporting of functions and variables" }, + { "name": "perl-extutils-makemaker", "uri": "https://anaconda.org/anaconda/perl-extutils-makemaker", "description": "Create a module Makefile" }, + { "name": "perl-file-which", "uri": "https://anaconda.org/anaconda/perl-file-which", "description": "Perl implementation of the which utility as an API" }, + { "name": "perl-getopt-tabular", "uri": "https://anaconda.org/anaconda/perl-getopt-tabular", "description": "table-driven argument parsing for Perl 5" }, + { "name": "perl-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-libs-amzn2-aarch64", "description": "(CDT) The libraries for the perl runtime" }, + { "name": "perl-regexp-common", "uri": "https://anaconda.org/anaconda/perl-regexp-common", "description": "Provide commonly requested regular expressions" }, + { "name": "perl-xml-parser", "uri": "https://anaconda.org/anaconda/perl-xml-parser", "description": "A perl module for parsing XML documents" }, + { "name": "perl-xml-parser-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/perl-xml-parser-amzn2-aarch64", "description": "(CDT) Perl module for parsing XML documents" }, + { "name": "persistent", "uri": "https://anaconda.org/anaconda/persistent", "description": "Translucent persistent objects" }, + { "name": "pg8000", "uri": "https://anaconda.org/anaconda/pg8000", "description": "PostgreSQL interface library" }, + { "name": "phik", "uri": "https://anaconda.org/anaconda/phik", "description": "Phi_K correlation analyzer library" }, + { "name": "phonenumbers", "uri": "https://anaconda.org/anaconda/phonenumbers", "description": "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers." }, + { "name": "picklable-itertools", "uri": "https://anaconda.org/anaconda/picklable-itertools", "description": "No Summary" }, + { "name": "pickle5", "uri": "https://anaconda.org/anaconda/pickle5", "description": "Experimental backport of the pickle 5 protocol (PEP 574)" }, + { "name": "pillow", "uri": "https://anaconda.org/anaconda/pillow", "description": "Pillow is the friendly PIL fork by Alex Clark and Contributors" }, + { "name": "pims", "uri": "https://anaconda.org/anaconda/pims", "description": "Python Image Sequence. Load video and sequential images in many formats with a simple, consistent interface." }, + { "name": "pip", "uri": "https://anaconda.org/anaconda/pip", "description": "PyPA recommended tool for installing Python packages" }, + { "name": "pipenv", "uri": "https://anaconda.org/anaconda/pipenv", "description": "Python Development Workflow for Humans." }, + { "name": "pivottablejs", "uri": "https://anaconda.org/anaconda/pivottablejs", "description": "No Summary" }, + { "name": "pivottablejs-airgap", "uri": "https://anaconda.org/anaconda/pivottablejs-airgap", "description": "PivotTable.js integration for Jupyter/IPython Notebook" }, + { "name": "pixman-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pixman-amzn2-aarch64", "description": "(CDT) Pixel manipulation library" }, + { "name": "pixman-cos6-i686", "uri": "https://anaconda.org/anaconda/pixman-cos6-i686", "description": "(CDT) Pixel manipulation library" }, + { "name": "pixman-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pixman-cos6-x86_64", "description": "(CDT) Pixel manipulation library" }, + { "name": "pixman-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/pixman-cos7-ppc64le", "description": "(CDT) Pixel manipulation library" }, + { "name": "pixman-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/pixman-devel-cos6-i686", "description": "(CDT) Pixel manipulation library development package" }, + { "name": "pkce", "uri": "https://anaconda.org/anaconda/pkce", "description": "PKCE Python generator." }, + { "name": "pkgconfig", "uri": "https://anaconda.org/anaconda/pkgconfig", "description": "A Python interface to the pkg-config command line tool" }, + { "name": "pkgconfig-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/pkgconfig-amzn2-aarch64", "description": "(CDT) A tool for determining compilation options" }, + { "name": "pkginfo", "uri": "https://anaconda.org/anaconda/pkginfo", "description": "Query metadatdata from sdists / bdists / installed packages." }, + { "name": "pkgutil-resolve-name", "uri": "https://anaconda.org/anaconda/pkgutil-resolve-name", "description": "Backport of Python 3.9's pkgutil.resolve_name; resolves a name to an object." }, + { "name": "plaster", "uri": "https://anaconda.org/anaconda/plaster", "description": "A loader interface around multiple config file formats." }, + { "name": "plaster_pastedeploy", "uri": "https://anaconda.org/anaconda/plaster_pastedeploy", "description": "A loader implementing the PasteDeploy syntax to be used by plaster." }, + { "name": "platformdirs", "uri": "https://anaconda.org/anaconda/platformdirs", "description": "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." }, + { "name": "plotly", "uri": "https://anaconda.org/anaconda/plotly", "description": "An interactive JavaScript-based visualization library for Python" }, + { "name": "plotly-resampler", "uri": "https://anaconda.org/anaconda/plotly-resampler", "description": "Visualizing large time series with plotly" }, + { "name": "plotnine", "uri": "https://anaconda.org/anaconda/plotnine", "description": "A grammar of graphics for python" }, + { "name": "pluggy", "uri": "https://anaconda.org/anaconda/pluggy", "description": "Plugin registration and hook calling for Python" }, + { "name": "ply", "uri": "https://anaconda.org/anaconda/ply", "description": "Python Lex-Yacc" }, + { "name": "plyvel", "uri": "https://anaconda.org/anaconda/plyvel", "description": "Plyvel, a fast and feature-rich Python interface to LevelDB" }, + { "name": "pmdarima", "uri": "https://anaconda.org/anaconda/pmdarima", "description": "Pmdarima (originally pyramid-arima, for the anagram of 'py' + 'arima') is a statistical library designed to fill the void in Python's time series analysis capabilities." }, + { "name": "poetry", "uri": "https://anaconda.org/anaconda/poetry", "description": "Python dependency management and packaging made easy" }, + { "name": "poetry-core", "uri": "https://anaconda.org/anaconda/poetry-core", "description": "Poetry PEP 517 Build Backend" }, + { "name": "poetry-dynamic-versioning", "uri": "https://anaconda.org/anaconda/poetry-dynamic-versioning", "description": "Plugin for Poetry to enable dynamic versioning based on VCS tags" }, + { "name": "poetry-plugin-export", "uri": "https://anaconda.org/anaconda/poetry-plugin-export", "description": "Poetry plugin to export the dependencies to various formats" }, + { "name": "pointpats", "uri": "https://anaconda.org/anaconda/pointpats", "description": "Statistical analysis of planar point patterns." }, + { "name": "polly", "uri": "https://anaconda.org/anaconda/polly", "description": "LLVM Framework for High-Level Loop and Data-Locality Optimizations" }, + { "name": "pomegranate", "uri": "https://anaconda.org/anaconda/pomegranate", "description": "Pomegranate is a graphical models library for Python, implemented in Cython for speed." }, + { "name": "pooch", "uri": "https://anaconda.org/anaconda/pooch", "description": "A friend to fetch your data files" }, + { "name": "poppler", "uri": "https://anaconda.org/anaconda/poppler", "description": "The Poppler PDF manipulation library." }, + { "name": "poppler-cpp", "uri": "https://anaconda.org/anaconda/poppler-cpp", "description": "The Poppler PDF manipulation library." }, + { "name": "poppler-data", "uri": "https://anaconda.org/anaconda/poppler-data", "description": "Encoding data for the Poppler PDF manipulation library." }, + { "name": "poppler-qt", "uri": "https://anaconda.org/anaconda/poppler-qt", "description": "The Poppler PDF manipulation library." }, + { "name": "popt", "uri": "https://anaconda.org/anaconda/popt", "description": "Popt is a C library for parsing command line parameters." }, + { "name": "portalocker", "uri": "https://anaconda.org/anaconda/portalocker", "description": "Portalocker is a library to provide an easy API to file locking." }, + { "name": "portend", "uri": "https://anaconda.org/anaconda/portend", "description": "TCP port monitoring utilities" }, + { "name": "portpicker", "uri": "https://anaconda.org/anaconda/portpicker", "description": "A library to choose unique available network ports." }, + { "name": "postgresql", "uri": "https://anaconda.org/anaconda/postgresql", "description": "PostgreSQL is a powerful, open source object-relational database system." }, + { "name": "powerlaw", "uri": "https://anaconda.org/anaconda/powerlaw", "description": "Toolbox for testing if a probability distribution fits a power law" }, + { "name": "powershell_shortcut", "uri": "https://anaconda.org/anaconda/powershell_shortcut", "description": "Powershell shortcut creator for Windows (using menuinst)" }, + { "name": "powershell_shortcut_miniconda", "uri": "https://anaconda.org/anaconda/powershell_shortcut_miniconda", "description": "Powershell shortcut creator for Windows (using menuinst)" }, + { "name": "pox", "uri": "https://anaconda.org/anaconda/pox", "description": "utilities for filesystem exploration and automated builds" }, + { "name": "poyo", "uri": "https://anaconda.org/anaconda/poyo", "description": "A lightweight YAML Parser for Python" }, + { "name": "ppft", "uri": "https://anaconda.org/anaconda/ppft", "description": "distributed and parallel python" }, + { "name": "pre-commit", "uri": "https://anaconda.org/anaconda/pre-commit", "description": "A framework for managing and maintaining multi-language pre-commit hooks." }, + { "name": "pre_commit", "uri": "https://anaconda.org/anaconda/pre_commit", "description": "A framework for managing and maintaining multi-language pre-commit hooks." }, + { "name": "preshed", "uri": "https://anaconda.org/anaconda/preshed", "description": "Cython Hash Table for Pre-Hashed Keys" }, + { "name": "pretend", "uri": "https://anaconda.org/anaconda/pretend", "description": "A library for stubbing in Python" }, + { "name": "prettytable", "uri": "https://anaconda.org/anaconda/prettytable", "description": "Display tabular data in a visually appealing ASCII table format" }, + { "name": "prince", "uri": "https://anaconda.org/anaconda/prince", "description": "Multivariate exploratory data analysis in Python — PCA, CA, MCA, MFA, FAMD, GPA" }, + { "name": "priority", "uri": "https://anaconda.org/anaconda/priority", "description": "A pure-Python implementation of the HTTP/2 priority tree" }, + { "name": "prison", "uri": "https://anaconda.org/anaconda/prison", "description": "Python rison encoder/decoder" }, + { "name": "progress", "uri": "https://anaconda.org/anaconda/progress", "description": "Easy progress reporting for Python" }, + { "name": "progressbar2", "uri": "https://anaconda.org/anaconda/progressbar2", "description": "A Python Progressbar library to provide visual (yet text based) progress to long running operations." }, + { "name": "proj", "uri": "https://anaconda.org/anaconda/proj", "description": "Cartographic Projections and Coordinate Transformations Library" }, + { "name": "prometheus_client", "uri": "https://anaconda.org/anaconda/prometheus_client", "description": "Python client for the Prometheus monitoring system" }, + { "name": "prometheus_flask_exporter", "uri": "https://anaconda.org/anaconda/prometheus_flask_exporter", "description": "Prometheus metrics exporter for Flask" }, + { "name": "promise", "uri": "https://anaconda.org/anaconda/promise", "description": "Ultra-performant Promise implementation in Python" }, + { "name": "prompt-toolkit", "uri": "https://anaconda.org/anaconda/prompt-toolkit", "description": "Library for building powerful interactive command lines in Python" }, + { "name": "prompt_toolkit", "uri": "https://anaconda.org/anaconda/prompt_toolkit", "description": "Library for building powerful interactive command lines in Python" }, + { "name": "propcache", "uri": "https://anaconda.org/anaconda/propcache", "description": "Accelerated property cache" }, + { "name": "prophet", "uri": "https://anaconda.org/anaconda/prophet", "description": "Automatic Forecasting Procedure" }, + { "name": "protego", "uri": "https://anaconda.org/anaconda/protego", "description": "A pure-Python robots.txt parser with support for modern conventions" }, + { "name": "proto-plus", "uri": "https://anaconda.org/anaconda/proto-plus", "description": "Beautiful, Pythonic protocol buffers." }, + { "name": "protobuf", "uri": "https://anaconda.org/anaconda/protobuf", "description": "Protocol Buffers - Google's data interchange format." }, + { "name": "pscript", "uri": "https://anaconda.org/anaconda/pscript", "description": "library for transpiling Python code to JavaScript." }, + { "name": "psqlodbc", "uri": "https://anaconda.org/anaconda/psqlodbc", "description": "psqlODBC is the official PostgreSQL ODBC Driver" }, + { "name": "psutil", "uri": "https://anaconda.org/anaconda/psutil", "description": "A cross-platform process and system utilities module for Python" }, + { "name": "psycopg2", "uri": "https://anaconda.org/anaconda/psycopg2", "description": "PostgreSQL database adapter for Python" }, + { "name": "ptscotch", "uri": "https://anaconda.org/anaconda/ptscotch", "description": "PT-SCOTCH: (Parallel) Static Mapping, Graph, Mesh and Hypergraph Partitioning, and Parallel and Sequential Sparse Matrix Ordering Package" }, + { "name": "pugixml", "uri": "https://anaconda.org/anaconda/pugixml", "description": "Light-weight, simple and fast XML parser for C++ with XPath support" }, + { "name": "pulseaudio-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pulseaudio-libs-cos6-x86_64", "description": "(CDT) Libraries for PulseAudio clients" }, + { "name": "pulseaudio-libs-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pulseaudio-libs-devel-cos6-x86_64", "description": "(CDT) Headers and libraries for PulseAudio client development" }, + { "name": "pulseaudio-libs-glib2-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pulseaudio-libs-glib2-cos6-x86_64", "description": "(CDT) GLIB 2.x bindings for PulseAudio clients" }, + { "name": "pulseaudio-libs-zeroconf-cos6-x86_64", "uri": "https://anaconda.org/anaconda/pulseaudio-libs-zeroconf-cos6-x86_64", "description": "(CDT) Zeroconf support for PulseAudio clients" }, + { "name": "pure_eval", "uri": "https://anaconda.org/anaconda/pure_eval", "description": "Safely evaluate AST nodes without side effects" }, + { "name": "py-boost", "uri": "https://anaconda.org/anaconda/py-boost", "description": "Free peer-reviewed portable C++ source libraries." }, + { "name": "py-cpuinfo", "uri": "https://anaconda.org/anaconda/py-cpuinfo", "description": "A module for getting CPU info with Python 2 & 3" }, + { "name": "py-lief", "uri": "https://anaconda.org/anaconda/py-lief", "description": "A cross platform library to parse, modify and abstract ELF, PE and MachO formats." }, + { "name": "py-mxnet", "uri": "https://anaconda.org/anaconda/py-mxnet", "description": "MXNet is a deep learning framework designed for both efficiency and flexibility" }, + { "name": "py-opencv", "uri": "https://anaconda.org/anaconda/py-opencv", "description": "Computer vision and machine learning software library." }, + { "name": "py-partiql-parser", "uri": "https://anaconda.org/anaconda/py-partiql-parser", "description": "Pure Python PartiQL Parser" }, + { "name": "py-spy", "uri": "https://anaconda.org/anaconda/py-spy", "description": "Sampling profiler for Python programs" }, + { "name": "py-xgboost", "uri": "https://anaconda.org/anaconda/py-xgboost", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "py-xgboost-cpu", "uri": "https://anaconda.org/anaconda/py-xgboost-cpu", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "py-xgboost-gpu", "uri": "https://anaconda.org/anaconda/py-xgboost-gpu", "description": "No Summary" }, + { "name": "py4j", "uri": "https://anaconda.org/anaconda/py4j", "description": "Enables Python programs to dynamically access arbitrary Java objects" }, + { "name": "py7zr", "uri": "https://anaconda.org/anaconda/py7zr", "description": "Pure python 7-zip library" }, + { "name": "pyamg", "uri": "https://anaconda.org/anaconda/pyamg", "description": "Algebraic Multigrid Solvers in Python" }, + { "name": "pyaml", "uri": "https://anaconda.org/anaconda/pyaml", "description": "PyYAML-based module to produce pretty and readable YAML-serialized data" }, + { "name": "pyarrow", "uri": "https://anaconda.org/anaconda/pyarrow", "description": "Python libraries for Apache Arrow" }, + { "name": "pyasn1", "uri": "https://anaconda.org/anaconda/pyasn1", "description": "ASN.1 types and codecs" }, + { "name": "pybcj", "uri": "https://anaconda.org/anaconda/pybcj", "description": "bcj filter library" }, + { "name": "pybind11", "uri": "https://anaconda.org/anaconda/pybind11", "description": "Seamless operability between C++11 and Python" }, + { "name": "pybind11-abi", "uri": "https://anaconda.org/anaconda/pybind11-abi", "description": "Seamless operability between C++11 and Python" }, + { "name": "pybind11-global", "uri": "https://anaconda.org/anaconda/pybind11-global", "description": "Seamless operability between C++11 and Python" }, + { "name": "pycares", "uri": "https://anaconda.org/anaconda/pycares", "description": "Python interface for c-ares" }, + { "name": "pyclipper", "uri": "https://anaconda.org/anaconda/pyclipper", "description": "Cython wrapper for the C++ translation of the Angus Johnson's Clipper library (ver. 6.4.2)" }, + { "name": "pycodestyle", "uri": "https://anaconda.org/anaconda/pycodestyle", "description": "Python style guide checker" }, + { "name": "pycosat", "uri": "https://anaconda.org/anaconda/pycosat", "description": "Bindings to picosat (a SAT solver)" }, + { "name": "pycryptodome", "uri": "https://anaconda.org/anaconda/pycryptodome", "description": "Cryptographic library for Python" }, + { "name": "pycryptodomex", "uri": "https://anaconda.org/anaconda/pycryptodomex", "description": "Cryptographic library for Python" }, + { "name": "pycryptosat", "uri": "https://anaconda.org/anaconda/pycryptosat", "description": "An advanced SAT Solver https://www.msoos.org" }, + { "name": "pyct", "uri": "https://anaconda.org/anaconda/pyct", "description": "python package common tasks for users (e.g. copy examples, fetch data, ...)" }, + { "name": "pyct-core", "uri": "https://anaconda.org/anaconda/pyct-core", "description": "Common tasks for package building (e.g. bundle examples)" }, + { "name": "pycurl", "uri": "https://anaconda.org/anaconda/pycurl", "description": "A Python Interface To The cURL library" }, + { "name": "pydantic", "uri": "https://anaconda.org/anaconda/pydantic", "description": "Data validation and settings management using python type hinting" }, + { "name": "pydantic-core", "uri": "https://anaconda.org/anaconda/pydantic-core", "description": "Core validation logic for pydantic written in rust" }, + { "name": "pydantic-settings", "uri": "https://anaconda.org/anaconda/pydantic-settings", "description": "Settings management using Pydantic" }, + { "name": "pydata-google-auth", "uri": "https://anaconda.org/anaconda/pydata-google-auth", "description": "Helpers for authenticating to Google APIs from Python." }, + { "name": "pydeck", "uri": "https://anaconda.org/anaconda/pydeck", "description": "Widget for deck.gl maps" }, + { "name": "pydispatcher", "uri": "https://anaconda.org/anaconda/pydispatcher", "description": "No Summary" }, + { "name": "pydocstyle", "uri": "https://anaconda.org/anaconda/pydocstyle", "description": "Python docstring style checker (formerly pep257)" }, + { "name": "pydot", "uri": "https://anaconda.org/anaconda/pydot", "description": "Python interface to Graphviz's Dot" }, + { "name": "pydruid", "uri": "https://anaconda.org/anaconda/pydruid", "description": "A Python connector for Druid" }, + { "name": "pyee", "uri": "https://anaconda.org/anaconda/pyee", "description": "A port of node.js's EventEmitter to python." }, + { "name": "pyemd", "uri": "https://anaconda.org/anaconda/pyemd", "description": "A Python wrapper for the Earth Mover's Distance." }, + { "name": "pyepsg", "uri": "https://anaconda.org/anaconda/pyepsg", "description": "Easy access to the EPSG database via http://epsg.io/" }, + { "name": "pyerfa", "uri": "https://anaconda.org/anaconda/pyerfa", "description": "Python bindings for ERFA routines" }, + { "name": "pyface", "uri": "https://anaconda.org/anaconda/pyface", "description": "Traits-capable windowing framework" }, + { "name": "pyfakefs", "uri": "https://anaconda.org/anaconda/pyfakefs", "description": "A fake file system that mocks the Python file system modules." }, + { "name": "pyfastner", "uri": "https://anaconda.org/anaconda/pyfastner", "description": "A fast implementation of dictionary based named entity recognition." }, + { "name": "pyflakes", "uri": "https://anaconda.org/anaconda/pyflakes", "description": "Pyflakes analyzes programs and detects various errors." }, + { "name": "pygithub", "uri": "https://anaconda.org/anaconda/pygithub", "description": "Python library implementing the GitHub API v3" }, + { "name": "pygments", "uri": "https://anaconda.org/anaconda/pygments", "description": "Pygments is a generic syntax highlighter suitable for use in code hosting, forums, wikis or other applications that need to prettify source code." }, + { "name": "pygpu", "uri": "https://anaconda.org/anaconda/pygpu", "description": "Library to manipulate arrays on GPU" }, + { "name": "pygraphviz", "uri": "https://anaconda.org/anaconda/pygraphviz", "description": "Python interface to Graphviz" }, + { "name": "pyhamcrest", "uri": "https://anaconda.org/anaconda/pyhamcrest", "description": "Hamcrest framework for matcher objects" }, + { "name": "pyicu", "uri": "https://anaconda.org/anaconda/pyicu", "description": "Welcome to PyICU, a Python extension wrapping the ICU C++ libraries." }, + { "name": "pyinotify", "uri": "https://anaconda.org/anaconda/pyinotify", "description": "Monitoring filesystems events with inotify on Linux." }, + { "name": "pyinstaller", "uri": "https://anaconda.org/anaconda/pyinstaller", "description": "PyInstaller bundles a Python application and all its dependencies into a single package." }, + { "name": "pyinstaller-hooks-contrib", "uri": "https://anaconda.org/anaconda/pyinstaller-hooks-contrib", "description": "Community maintained hooks for PyInstaller" }, + { "name": "pyjks", "uri": "https://anaconda.org/anaconda/pyjks", "description": "Pure-Python Java Keystore (JKS) library" }, + { "name": "pyjsparser", "uri": "https://anaconda.org/anaconda/pyjsparser", "description": "Fast javascript parser (based on esprima.js)" }, + { "name": "pyjwt", "uri": "https://anaconda.org/anaconda/pyjwt", "description": "JSON Web Token implementation in Python" }, + { "name": "pykdtree", "uri": "https://anaconda.org/anaconda/pykdtree", "description": "Fast kd-tree implementation with OpenMP-enabled queries" }, + { "name": "pykerberos", "uri": "https://anaconda.org/anaconda/pykerberos", "description": "high-level interface to Kerberos" }, + { "name": "pykrb5", "uri": "https://anaconda.org/anaconda/pykrb5", "description": "Kerberos API bindings for Python" }, + { "name": "pylev", "uri": "https://anaconda.org/anaconda/pylev", "description": "A pure Python Levenshtein implementation that's not freaking GPL'd." }, + { "name": "pylint", "uri": "https://anaconda.org/anaconda/pylint", "description": "python code static checker" }, + { "name": "pylint-venv", "uri": "https://anaconda.org/anaconda/pylint-venv", "description": "pylint-venv provides a Pylint init-hook to use the same Pylint installation with different virtual environments." }, + { "name": "pyls-black", "uri": "https://anaconda.org/anaconda/pyls-black", "description": "Black plugin for the Python Language Server" }, + { "name": "pyls-spyder", "uri": "https://anaconda.org/anaconda/pyls-spyder", "description": "Spyder extensions for the python-lsp-server" }, + { "name": "pymc", "uri": "https://anaconda.org/anaconda/pymc", "description": "PyMC: Bayesian Stochastic Modelling in Python" }, + { "name": "pymc-experimental", "uri": "https://anaconda.org/anaconda/pymc-experimental", "description": "The next batch of cool PyMC features" }, + { "name": "pymc3", "uri": "https://anaconda.org/anaconda/pymc3", "description": "Probabilistic Programming in Python" }, + { "name": "pymdown-extensions", "uri": "https://anaconda.org/anaconda/pymdown-extensions", "description": "Extension pack for Python Markdown." }, + { "name": "pymeeus", "uri": "https://anaconda.org/anaconda/pymeeus", "description": "Python implementation of Jean Meeus astronomical routines" }, + { "name": "pymongo", "uri": "https://anaconda.org/anaconda/pymongo", "description": "Python driver for MongoDB http://www.mongodb.org" }, + { "name": "pymorphy3", "uri": "https://anaconda.org/anaconda/pymorphy3", "description": "Morphological analyzer (POS tagger + inflection engine) for Ukrainian and Russian languages." }, + { "name": "pymorphy3-dicts-ru", "uri": "https://anaconda.org/anaconda/pymorphy3-dicts-ru", "description": "Russian dictionaries for pymorphy3" }, + { "name": "pymorphy3-dicts-uk", "uri": "https://anaconda.org/anaconda/pymorphy3-dicts-uk", "description": "Ukrainian dictionaries for pymorphy3" }, + { "name": "pympler", "uri": "https://anaconda.org/anaconda/pympler", "description": "Development tool to measure, monitor and analyze the memory behavior of Python objects in a running Python application." }, + { "name": "pymysql", "uri": "https://anaconda.org/anaconda/pymysql", "description": "Pure Python MySQL Driver" }, + { "name": "pynacl", "uri": "https://anaconda.org/anaconda/pynacl", "description": "PyNaCl is a Python binding to the Networking and Cryptography library, a crypto library with the stated goal of improving usability, security and speed." }, + { "name": "pynndescent", "uri": "https://anaconda.org/anaconda/pynndescent", "description": "Simple fast approximate nearest neighbor search" }, + { "name": "pyo3-pack", "uri": "https://anaconda.org/anaconda/pyo3-pack", "description": "No Summary" }, + { "name": "pyobjc-core", "uri": "https://anaconda.org/anaconda/pyobjc-core", "description": "Python<->ObjC Interoperability Module" }, + { "name": "pyobjc-framework-cocoa", "uri": "https://anaconda.org/anaconda/pyobjc-framework-cocoa", "description": "Wrappers for the Cocoa frameworks on Mac OS X" }, + { "name": "pyobjc-framework-coreservices", "uri": "https://anaconda.org/anaconda/pyobjc-framework-coreservices", "description": "Wrappers for the “CoreServices” framework on macOS." }, + { "name": "pyobjc-framework-fsevents", "uri": "https://anaconda.org/anaconda/pyobjc-framework-fsevents", "description": "Wrappers for the framework FSEvents on macOS" }, + { "name": "pyod", "uri": "https://anaconda.org/anaconda/pyod", "description": "A Python Toolkit for Scalable Outlier Detection (Anomaly Detection)" }, + { "name": "pyodbc", "uri": "https://anaconda.org/anaconda/pyodbc", "description": "DB API Module for ODBC" }, + { "name": "pyomniscidb", "uri": "https://anaconda.org/anaconda/pyomniscidb", "description": "A python DB API 2 compatible client for OmniSci (formerly MapD)." }, + { "name": "pyomniscidbe", "uri": "https://anaconda.org/anaconda/pyomniscidbe", "description": "The OmniSci / HeavyDB database" }, + { "name": "pyopengl", "uri": "https://anaconda.org/anaconda/pyopengl", "description": "No Summary" }, + { "name": "pyopenssl", "uri": "https://anaconda.org/anaconda/pyopenssl", "description": "Python wrapper module around the OpenSSL library" }, + { "name": "pyparsing", "uri": "https://anaconda.org/anaconda/pyparsing", "description": "Create and execute simple grammars" }, + { "name": "pypdf", "uri": "https://anaconda.org/anaconda/pypdf", "description": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" }, + { "name": "pypdf-with-crypto", "uri": "https://anaconda.org/anaconda/pypdf-with-crypto", "description": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" }, + { "name": "pypdf-with-full", "uri": "https://anaconda.org/anaconda/pypdf-with-full", "description": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" }, + { "name": "pypdf-with-image", "uri": "https://anaconda.org/anaconda/pypdf-with-image", "description": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" }, + { "name": "pypdf2", "uri": "https://anaconda.org/anaconda/pypdf2", "description": "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" }, + { "name": "pyperf", "uri": "https://anaconda.org/anaconda/pyperf", "description": "Toolkit to run Python benchmarks" }, + { "name": "pyperformance", "uri": "https://anaconda.org/anaconda/pyperformance", "description": "Python benchmark suite" }, + { "name": "pyphen", "uri": "https://anaconda.org/anaconda/pyphen", "description": "Pure Python module to hyphenate text" }, + { "name": "pypng", "uri": "https://anaconda.org/anaconda/pypng", "description": "Pure Python PNG image encoder and decoder" }, + { "name": "pyppmd", "uri": "https://anaconda.org/anaconda/pyppmd", "description": "PPMd compression/decompression library" }, + { "name": "pyprof2calltree", "uri": "https://anaconda.org/anaconda/pyprof2calltree", "description": "Help visualize profiling data from cProfile with qcachegrind" }, + { "name": "pyproj", "uri": "https://anaconda.org/anaconda/pyproj", "description": "Python interface to PROJ library" }, + { "name": "pyproject-metadata", "uri": "https://anaconda.org/anaconda/pyproject-metadata", "description": "PEP 621 metadata parsing" }, + { "name": "pyproject_hooks", "uri": "https://anaconda.org/anaconda/pyproject_hooks", "description": "Wrappers to call pyproject.toml-based build backend hooks." }, + { "name": "pyqt", "uri": "https://anaconda.org/anaconda/pyqt", "description": "Python bindings for the Qt cross platform application toolkit" }, + { "name": "pyqt-builder", "uri": "https://anaconda.org/anaconda/pyqt-builder", "description": "The PEP 517 compliant PyQt build system" }, + { "name": "pyqt5-sip", "uri": "https://anaconda.org/anaconda/pyqt5-sip", "description": "Python bindings for the Qt cross platform application toolkit" }, + { "name": "pyqtchart", "uri": "https://anaconda.org/anaconda/pyqtchart", "description": "Python bindings for the Qt cross platform application toolkit" }, + { "name": "pyqtgraph", "uri": "https://anaconda.org/anaconda/pyqtgraph", "description": "Scientific Graphics and GUI Library for Python" }, + { "name": "pyqtwebengine", "uri": "https://anaconda.org/anaconda/pyqtwebengine", "description": "Python bindings for the Qt cross platform application toolkit" }, + { "name": "pyquery", "uri": "https://anaconda.org/anaconda/pyquery", "description": "A jquery-like library for python" }, + { "name": "pyreadline3", "uri": "https://anaconda.org/anaconda/pyreadline3", "description": "A python implementation of GNU readline." }, + { "name": "pyrsistent", "uri": "https://anaconda.org/anaconda/pyrsistent", "description": "Persistent/Functional/Immutable data structures" }, + { "name": "pyrush", "uri": "https://anaconda.org/anaconda/pyrush", "description": "A fast implementation of RuSH (Rule-based sentence Segmenter using Hashing)." }, + { "name": "pysbd", "uri": "https://anaconda.org/anaconda/pysbd", "description": "Rule-based sentence boundary detection" }, + { "name": "pyserial", "uri": "https://anaconda.org/anaconda/pyserial", "description": "Python serial port access library" }, + { "name": "pysftp", "uri": "https://anaconda.org/anaconda/pysftp", "description": "A friendly face on SFTP" }, + { "name": "pyshp", "uri": "https://anaconda.org/anaconda/pyshp", "description": "Pure Python read/write support for ESRI Shapefile format" }, + { "name": "pysimstring", "uri": "https://anaconda.org/anaconda/pysimstring", "description": "Python Simstring bindings for Linux, OS X and Windows" }, + { "name": "pysmbclient", "uri": "https://anaconda.org/anaconda/pysmbclient", "description": "A convenient smbclient wrapper" }, + { "name": "pysmi", "uri": "https://anaconda.org/anaconda/pysmi", "description": "SNMP SMI/MIB Parser" }, + { "name": "pysnmp", "uri": "https://anaconda.org/anaconda/pysnmp", "description": "SNMP library for Python" }, + { "name": "pysocks", "uri": "https://anaconda.org/anaconda/pysocks", "description": "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." }, + { "name": "pyspark", "uri": "https://anaconda.org/anaconda/pyspark", "description": "Apache Spark Python API" }, + { "name": "pyspnego", "uri": "https://anaconda.org/anaconda/pyspnego", "description": "Windows Negotiate Authentication Client and Server" }, + { "name": "pytables", "uri": "https://anaconda.org/anaconda/pytables", "description": "Brings together Python, HDF5 and NumPy to easily handle large amounts of data." }, + { "name": "pyte", "uri": "https://anaconda.org/anaconda/pyte", "description": "Simple VTXXX-compatible linux terminal emulator" }, + { "name": "pytensor", "uri": "https://anaconda.org/anaconda/pytensor", "description": "An optimizing compiler for evaluating mathematical expressions." }, + { "name": "pytesseract", "uri": "https://anaconda.org/anaconda/pytesseract", "description": "Python-tesseract is an optical character recognition (OCR) tool for python." }, + { "name": "pytest", "uri": "https://anaconda.org/anaconda/pytest", "description": "Simple and powerful testing with Python." }, + { "name": "pytest-arraydiff", "uri": "https://anaconda.org/anaconda/pytest-arraydiff", "description": "pytest plugin to help with comparing array output from tests" }, + { "name": "pytest-astropy", "uri": "https://anaconda.org/anaconda/pytest-astropy", "description": "Meta-package containing dependencies for testing Astropy" }, + { "name": "pytest-astropy-header", "uri": "https://anaconda.org/anaconda/pytest-astropy-header", "description": "Pytest plugin to add diagnostic information to the header of the test output" }, + { "name": "pytest-asyncio", "uri": "https://anaconda.org/anaconda/pytest-asyncio", "description": "Pytest support for asyncio" }, + { "name": "pytest-azurepipelines", "uri": "https://anaconda.org/anaconda/pytest-azurepipelines", "description": "Plugin for pytest that makes it simple to work with Azure Pipelines" }, + { "name": "pytest-base-url", "uri": "https://anaconda.org/anaconda/pytest-base-url", "description": "pytest plugin for URL based testing" }, + { "name": "pytest-bdd", "uri": "https://anaconda.org/anaconda/pytest-bdd", "description": "BDD for pytest" }, + { "name": "pytest-benchmark", "uri": "https://anaconda.org/anaconda/pytest-benchmark", "description": "A py.test fixture for benchmarking code" }, + { "name": "pytest-cache", "uri": "https://anaconda.org/anaconda/pytest-cache", "description": "No Summary" }, + { "name": "pytest-codspeed", "uri": "https://anaconda.org/anaconda/pytest-codspeed", "description": "Pytest plugin to create CodSpeed benchmarks" }, + { "name": "pytest-console-scripts", "uri": "https://anaconda.org/anaconda/pytest-console-scripts", "description": "Pytest plugin for testing console scripts" }, + { "name": "pytest-cov", "uri": "https://anaconda.org/anaconda/pytest-cov", "description": "Pytest plugin for measuring coverage" }, + { "name": "pytest-csv", "uri": "https://anaconda.org/anaconda/pytest-csv", "description": "CSV output for pytest." }, + { "name": "pytest-dependency", "uri": "https://anaconda.org/anaconda/pytest-dependency", "description": "Manage dependencies of tests" }, + { "name": "pytest-describe", "uri": "https://anaconda.org/anaconda/pytest-describe", "description": "Describe-style plugin for py.test" }, + { "name": "pytest-doctestplus", "uri": "https://anaconda.org/anaconda/pytest-doctestplus", "description": "Pytest plugin with advanced doctest features." }, + { "name": "pytest-filter-subpackage", "uri": "https://anaconda.org/anaconda/pytest-filter-subpackage", "description": "Pytest plugin for filtering based on sub-packages" }, + { "name": "pytest-flake8", "uri": "https://anaconda.org/anaconda/pytest-flake8", "description": "pytest plugin to check FLAKE8 requirements" }, + { "name": "pytest-flakefinder", "uri": "https://anaconda.org/anaconda/pytest-flakefinder", "description": "Runs tests multiple times to expose flakiness." }, + { "name": "pytest-flakes", "uri": "https://anaconda.org/anaconda/pytest-flakes", "description": "pytest plugin to check source code with pyflakes" }, + { "name": "pytest-forked", "uri": "https://anaconda.org/anaconda/pytest-forked", "description": "run tests in isolated forked subprocesses" }, + { "name": "pytest-grpc", "uri": "https://anaconda.org/anaconda/pytest-grpc", "description": "Write test for gRPC with pytest" }, + { "name": "pytest-html", "uri": "https://anaconda.org/anaconda/pytest-html", "description": "pytest plugin for generating HTML reports" }, + { "name": "pytest-httpserver", "uri": "https://anaconda.org/anaconda/pytest-httpserver", "description": "pytest-httpserver is a httpserver for pytest" }, + { "name": "pytest-json", "uri": "https://anaconda.org/anaconda/pytest-json", "description": "Generate JSON test reports" }, + { "name": "pytest-jupyter", "uri": "https://anaconda.org/anaconda/pytest-jupyter", "description": "Pytest plugin that provides a set of fixtures and markers for testing Jupyter notebooks and\nJupyter kernel sessions using the Pytest framework. This package allows developers to run\ntests on Jupyter notebooks as if they were regular Python modules." }, + { "name": "pytest-jupyter-client", "uri": "https://anaconda.org/anaconda/pytest-jupyter-client", "description": "Pytest plugin that provides a set of fixtures and markers for testing Jupyter notebooks and\nJupyter kernel sessions using the Pytest framework. This package allows developers to run\ntests on Jupyter notebooks as if they were regular Python modules." }, + { "name": "pytest-jupyter-server", "uri": "https://anaconda.org/anaconda/pytest-jupyter-server", "description": "Pytest plugin that provides a set of fixtures and markers for testing Jupyter notebooks and\nJupyter kernel sessions using the Pytest framework. This package allows developers to run\ntests on Jupyter notebooks as if they were regular Python modules." }, + { "name": "pytest-localserver", "uri": "https://anaconda.org/anaconda/pytest-localserver", "description": "pytest-localserver is a plugin for the pytest testing framework which enables you to test server connections locally." }, + { "name": "pytest-metadata", "uri": "https://anaconda.org/anaconda/pytest-metadata", "description": "pytest plugin for test session metadata" }, + { "name": "pytest-mock", "uri": "https://anaconda.org/anaconda/pytest-mock", "description": "Thin-wrapper around the mock package for easier use with py.test" }, + { "name": "pytest-mpi", "uri": "https://anaconda.org/anaconda/pytest-mpi", "description": "Pytest plugin for working with MPI" }, + { "name": "pytest-openfiles", "uri": "https://anaconda.org/anaconda/pytest-openfiles", "description": "Pytest plugin for detecting inadvertent open file handles" }, + { "name": "pytest-ordering", "uri": "https://anaconda.org/anaconda/pytest-ordering", "description": "pytest plugin to run your tests in a specific order" }, + { "name": "pytest-pep8", "uri": "https://anaconda.org/anaconda/pytest-pep8", "description": "No Summary" }, + { "name": "pytest-qt", "uri": "https://anaconda.org/anaconda/pytest-qt", "description": "pytest support for PyQt and PySide applications" }, + { "name": "pytest-randomly", "uri": "https://anaconda.org/anaconda/pytest-randomly", "description": "Pytest plugin to randomly order tests and control random.seed" }, + { "name": "pytest-remotedata", "uri": "https://anaconda.org/anaconda/pytest-remotedata", "description": "Pytest plugin for controlling remote data access" }, + { "name": "pytest-replay", "uri": "https://anaconda.org/anaconda/pytest-replay", "description": "Saves shell scripts that allow re-execute previous pytest runs to reproduce crashes or flaky tests" }, + { "name": "pytest-rerunfailures", "uri": "https://anaconda.org/anaconda/pytest-rerunfailures", "description": "pytest plugin to re-run tests to eliminate flaky failures" }, + { "name": "pytest-runner", "uri": "https://anaconda.org/anaconda/pytest-runner", "description": "Invoke py.test as distutils command with dependency resolution." }, + { "name": "pytest-selenium", "uri": "https://anaconda.org/anaconda/pytest-selenium", "description": "pytest-selenium is a plugin for py.test that provides support for running Selenium based tests." }, + { "name": "pytest-shard", "uri": "https://anaconda.org/anaconda/pytest-shard", "description": "Shards tests based on a hash of their test name." }, + { "name": "pytest-socket", "uri": "https://anaconda.org/anaconda/pytest-socket", "description": "Pytest Plugin to disable socket calls during tests" }, + { "name": "pytest-subprocess", "uri": "https://anaconda.org/anaconda/pytest-subprocess", "description": "A plugin to fake subprocess for pytest" }, + { "name": "pytest-subtests", "uri": "https://anaconda.org/anaconda/pytest-subtests", "description": "unittest subTest() support and subtests fixture" }, + { "name": "pytest-sugar", "uri": "https://anaconda.org/anaconda/pytest-sugar", "description": "Pytest plugin that adds a progress bar and other visual enhancements" }, + { "name": "pytest-timeout", "uri": "https://anaconda.org/anaconda/pytest-timeout", "description": "This is a plugin which will terminate tests after a certain timeout." }, + { "name": "pytest-tornado", "uri": "https://anaconda.org/anaconda/pytest-tornado", "description": "A py.test plugin providing fixtures and markers to simplify testing of\nasynchronous tornado applications." }, + { "name": "pytest-tornasync", "uri": "https://anaconda.org/anaconda/pytest-tornasync", "description": "py.test plugin for testing Python 3.5+ Tornado code" }, + { "name": "pytest-trio", "uri": "https://anaconda.org/anaconda/pytest-trio", "description": "This is a pytest plugin to help you test projects that use Trio, a friendly library for concurrency and async I/O in Python" }, + { "name": "pytest-variables", "uri": "https://anaconda.org/anaconda/pytest-variables", "description": "pytest plugin for providing variables to tests/fixtures" }, + { "name": "pytest-xdist", "uri": "https://anaconda.org/anaconda/pytest-xdist", "description": "py.test xdist plugin for distributed testing and loop-on-failing modes" }, + { "name": "python", "uri": "https://anaconda.org/anaconda/python", "description": "General purpose programming language" }, + { "name": "python-bidi", "uri": "https://anaconda.org/anaconda/python-bidi", "description": "Pure python implementation of the BiDi layout algorithm" }, + { "name": "python-blosc", "uri": "https://anaconda.org/anaconda/python-blosc", "description": "A Python wrapper for the extremely fast Blosc compression library" }, + { "name": "python-build", "uri": "https://anaconda.org/anaconda/python-build", "description": "A simple, correct PEP517 package builder" }, + { "name": "python-chromedriver-binary", "uri": "https://anaconda.org/anaconda/python-chromedriver-binary", "description": "WebDriver for Chrome (binary)" }, + { "name": "python-clang", "uri": "https://anaconda.org/anaconda/python-clang", "description": "Development headers and libraries for Clang" }, + { "name": "python-crfsuite", "uri": "https://anaconda.org/anaconda/python-crfsuite", "description": "Python binding for CRFsuite" }, + { "name": "python-daemon", "uri": "https://anaconda.org/anaconda/python-daemon", "description": "Library to implement a well-behaved Unix daemon process." }, + { "name": "python-dateutil", "uri": "https://anaconda.org/anaconda/python-dateutil", "description": "Extensions to the standard Python datetime module" }, + { "name": "python-debian", "uri": "https://anaconda.org/anaconda/python-debian", "description": "Debian package related modules" }, + { "name": "python-dotenv", "uri": "https://anaconda.org/anaconda/python-dotenv", "description": "Get and set values in your .env file in local and production servers like Heroku does." }, + { "name": "python-editor", "uri": "https://anaconda.org/anaconda/python-editor", "description": "Programmatically open an editor, capture the result." }, + { "name": "python-engineio", "uri": "https://anaconda.org/anaconda/python-engineio", "description": "Engine.IO server" }, + { "name": "python-fastjsonschema", "uri": "https://anaconda.org/anaconda/python-fastjsonschema", "description": "Fastest Python implementation of JSON schema" }, + { "name": "python-flatbuffers", "uri": "https://anaconda.org/anaconda/python-flatbuffers", "description": "The FlatBuffers serialization format for Python" }, + { "name": "python-gil", "uri": "https://anaconda.org/anaconda/python-gil", "description": "General purpose programming language" }, + { "name": "python-graphviz", "uri": "https://anaconda.org/anaconda/python-graphviz", "description": "Simple Python interface for Graphviz" }, + { "name": "python-gssapi", "uri": "https://anaconda.org/anaconda/python-gssapi", "description": "A Python interface to RFC 2743/2744 (plus common extensions)" }, + { "name": "python-hdfs", "uri": "https://anaconda.org/anaconda/python-hdfs", "description": "HdfsCLI: API and command line interface for HDFS." }, + { "name": "python-installer", "uri": "https://anaconda.org/anaconda/python-installer", "description": "A library for installing Python wheels." }, + { "name": "python-isal", "uri": "https://anaconda.org/anaconda/python-isal", "description": "Faster zlib and gzip compatible compression and decompression by providing python bindings for the isa-l library." }, + { "name": "python-javapackages-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/python-javapackages-cos7-ppc64le", "description": "(CDT) Module for handling various files for Java packaging" }, + { "name": "python-javapackages-cos7-s390x", "uri": "https://anaconda.org/anaconda/python-javapackages-cos7-s390x", "description": "(CDT) Module for handling various files for Java packaging" }, + { "name": "python-jenkins", "uri": "https://anaconda.org/anaconda/python-jenkins", "description": "Python Jenkins is a python wrapper for the Jenkins REST API" }, + { "name": "python-jose", "uri": "https://anaconda.org/anaconda/python-jose", "description": "JOSE implementation in Python" }, + { "name": "python-json-logger", "uri": "https://anaconda.org/anaconda/python-json-logger", "description": "JSON Formatter for Python Logging" }, + { "name": "python-jsonrpc-server", "uri": "https://anaconda.org/anaconda/python-jsonrpc-server", "description": "A Python 2.7 and 3.4+ server implementation of the JSON RPC 2.0 protocol." }, + { "name": "python-kaleido", "uri": "https://anaconda.org/anaconda/python-kaleido", "description": "Fast static image export for web-based visualization libraries" }, + { "name": "python-kubernetes", "uri": "https://anaconda.org/anaconda/python-kubernetes", "description": "The official Kubernetes python client." }, + { "name": "python-language-server", "uri": "https://anaconda.org/anaconda/python-language-server", "description": "An implementation of the Language Server Protocol for Python" }, + { "name": "python-levenshtein", "uri": "https://anaconda.org/anaconda/python-levenshtein", "description": "Python extension for computing string edit distances and similarities." }, + { "name": "python-libarchive-c", "uri": "https://anaconda.org/anaconda/python-libarchive-c", "description": "Python interface to libarchive" }, + { "name": "python-lmdb", "uri": "https://anaconda.org/anaconda/python-lmdb", "description": "Universal Python binding for the LMDB 'Lightning' Database" }, + { "name": "python-louvain", "uri": "https://anaconda.org/anaconda/python-louvain", "description": "Louvain Community Detection" }, + { "name": "python-lsp-black", "uri": "https://anaconda.org/anaconda/python-lsp-black", "description": "Black plugin for the Python LSP Server" }, + { "name": "python-lsp-jsonrpc", "uri": "https://anaconda.org/anaconda/python-lsp-jsonrpc", "description": "A Python server implementation of the JSON RPC 2.0 protocol." }, + { "name": "python-lsp-server", "uri": "https://anaconda.org/anaconda/python-lsp-server", "description": "An implementation of the Language Server Protocol for Python" }, + { "name": "python-memcached", "uri": "https://anaconda.org/anaconda/python-memcached", "description": "Pure python memcached client" }, + { "name": "python-multipart", "uri": "https://anaconda.org/anaconda/python-multipart", "description": "A streaming multipart parser for Python." }, + { "name": "python-nvd3", "uri": "https://anaconda.org/anaconda/python-nvd3", "description": "Python NVD3 - Chart Library for d3.js" }, + { "name": "python-oauth2", "uri": "https://anaconda.org/anaconda/python-oauth2", "description": "OAuth 2.0 provider written in python" }, + { "name": "python-openid", "uri": "https://anaconda.org/anaconda/python-openid", "description": "OpenID support for servers and consumers." }, + { "name": "python-pptx", "uri": "https://anaconda.org/anaconda/python-pptx", "description": "Generate and manipulate Open XML PowerPoint (.pptx) files" }, + { "name": "python-rapidjson", "uri": "https://anaconda.org/anaconda/python-rapidjson", "description": "Python wrapper around rapidjson" }, + { "name": "python-regr-testsuite", "uri": "https://anaconda.org/anaconda/python-regr-testsuite", "description": "General purpose programming language" }, + { "name": "python-slugify", "uri": "https://anaconda.org/anaconda/python-slugify", "description": "A Python Slugify application that handles Unicode" }, + { "name": "python-snappy", "uri": "https://anaconda.org/anaconda/python-snappy", "description": "Python library for the snappy compression library from Google" }, + { "name": "python-socketio", "uri": "https://anaconda.org/anaconda/python-socketio", "description": "Socket.IO server" }, + { "name": "python-tblib", "uri": "https://anaconda.org/anaconda/python-tblib", "description": "Serialization library for Exceptions and Tracebacks." }, + { "name": "python-tzdata", "uri": "https://anaconda.org/anaconda/python-tzdata", "description": "Provider of IANA time zone data" }, + { "name": "python-utils", "uri": "https://anaconda.org/anaconda/python-utils", "description": "Python Utils is a collection of small Python functions and classes which make common patterns shorter and easier." }, + { "name": "python-xxhash", "uri": "https://anaconda.org/anaconda/python-xxhash", "description": "Python binding for xxHash" }, + { "name": "python-zstd", "uri": "https://anaconda.org/anaconda/python-zstd", "description": "ZSTD Bindings for Python" }, + { "name": "python.app", "uri": "https://anaconda.org/anaconda/python.app", "description": "Proxy on macOS letting Python libraries hook into the GUI event loop" }, + { "name": "python3-openid", "uri": "https://anaconda.org/anaconda/python3-openid", "description": "OpenID support for modern servers and consumers." }, + { "name": "python3-saml", "uri": "https://anaconda.org/anaconda/python3-saml", "description": "Onelogin Python Toolkit. Add SAML support to your Python software using this library" }, + { "name": "python_abi", "uri": "https://anaconda.org/anaconda/python_abi", "description": "Metapackage to select python implementation" }, + { "name": "python_http_client", "uri": "https://anaconda.org/anaconda/python_http_client", "description": "SendGrid's Python HTTP Client for calling APIs" }, + { "name": "pythonanywhere", "uri": "https://anaconda.org/anaconda/pythonanywhere", "description": "PythonAnywhere helper tools for users" }, + { "name": "pythonnet", "uri": "https://anaconda.org/anaconda/pythonnet", "description": ".Net and Mono integration for Python" }, + { "name": "pythran", "uri": "https://anaconda.org/anaconda/pythran", "description": "a claimless python to c++ converter" }, + { "name": "pytimeparse", "uri": "https://anaconda.org/anaconda/pytimeparse", "description": "A small Python library to parse various kinds of time expressions" }, + { "name": "pytoml", "uri": "https://anaconda.org/anaconda/pytoml", "description": "A TOML-0.4.0 parser/writer for Python." }, + { "name": "pytoolconfig", "uri": "https://anaconda.org/anaconda/pytoolconfig", "description": "Python tool configuration" }, + { "name": "pytorch", "uri": "https://anaconda.org/anaconda/pytorch", "description": "PyTorch is an optimized tensor library for deep learning using GPUs and CPUs." }, + { "name": "pytorch-cpu", "uri": "https://anaconda.org/anaconda/pytorch-cpu", "description": "PyTorch is an optimized tensor library for deep learning using GPUs and CPUs." }, + { "name": "pytorch-gpu", "uri": "https://anaconda.org/anaconda/pytorch-gpu", "description": "PyTorch is an optimized tensor library for deep learning using GPUs and CPUs." }, + { "name": "pytorch-lightning", "uri": "https://anaconda.org/anaconda/pytorch-lightning", "description": "PyTorch Lightning is the lightweight PyTorch wrapper for ML researchers. Scale your models. Write less boilerplate." }, + { "name": "pyts", "uri": "https://anaconda.org/anaconda/pyts", "description": "A Python package for time series classification" }, + { "name": "pytz", "uri": "https://anaconda.org/anaconda/pytz", "description": "World timezone definitions, modern and historical." }, + { "name": "pytzdata", "uri": "https://anaconda.org/anaconda/pytzdata", "description": "Official timezone database for Python" }, + { "name": "pyuca", "uri": "https://anaconda.org/anaconda/pyuca", "description": "a Python implementation of the Unicode Collation Algorithm" }, + { "name": "pyutilib", "uri": "https://anaconda.org/anaconda/pyutilib", "description": "PyUtilib: A collection of Python utilities" }, + { "name": "pyviz_comms", "uri": "https://anaconda.org/anaconda/pyviz_comms", "description": "Bidirectional notebook communication for HoloViz libraries" }, + { "name": "pywavelets", "uri": "https://anaconda.org/anaconda/pywavelets", "description": "Discrete Wavelet Transforms in Python" }, + { "name": "pywin32", "uri": "https://anaconda.org/anaconda/pywin32", "description": "No Summary" }, + { "name": "pywin32-ctypes", "uri": "https://anaconda.org/anaconda/pywin32-ctypes", "description": "A limited subset of pywin32 re-implemented using ctypes (or cffi)" }, + { "name": "pywinpty", "uri": "https://anaconda.org/anaconda/pywinpty", "description": "Pseudoterminals for Windows in Python" }, + { "name": "pywinrm", "uri": "https://anaconda.org/anaconda/pywinrm", "description": "Python library for Windows Remote Management (WinRM)" }, + { "name": "pyxdg", "uri": "https://anaconda.org/anaconda/pyxdg", "description": "PyXDG contains implementations of freedesktop.org standards in python." }, + { "name": "pyyaml", "uri": "https://anaconda.org/anaconda/pyyaml", "description": "YAML parser and emitter for Python" }, + { "name": "pyzmq", "uri": "https://anaconda.org/anaconda/pyzmq", "description": "Python bindings for zeromq" }, + { "name": "pyzstd", "uri": "https://anaconda.org/anaconda/pyzstd", "description": "Python bindings to Zstandard (zstd) compression library" }, + { "name": "qasync", "uri": "https://anaconda.org/anaconda/qasync", "description": "Implementation of the PEP 3156 Event-Loop with Qt." }, + { "name": "qbs", "uri": "https://anaconda.org/anaconda/qbs", "description": "Qbs (pronounced Cubes) is a cross-platform build tool" }, + { "name": "qdarkstyle", "uri": "https://anaconda.org/anaconda/qdarkstyle", "description": "A dark stylesheet for Qt applications (Qt4, Qt5, PySide, PyQt4, PyQt5, QtPy, PyQtGraph)." }, + { "name": "qdldl-python", "uri": "https://anaconda.org/anaconda/qdldl-python", "description": "Python interface to the QDLDL free LDL factorization routine for quasi-definite linear systems" }, + { "name": "qds-sdk", "uri": "https://anaconda.org/anaconda/qds-sdk", "description": "Python SDK for coding to the Qubole Data Service API" }, + { "name": "qgrid", "uri": "https://anaconda.org/anaconda/qgrid", "description": "Pandas DataFrame viewer for Jupyter Notebook" }, + { "name": "qhull", "uri": "https://anaconda.org/anaconda/qhull", "description": "Qhull computes the convex hull" }, + { "name": "qpd", "uri": "https://anaconda.org/anaconda/qpd", "description": "Query Pandas Using SQL" }, + { "name": "qrcode", "uri": "https://anaconda.org/anaconda/qrcode", "description": "QR Code image generator" }, + { "name": "qrencode-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/qrencode-libs-amzn2-aarch64", "description": "(CDT) QR Code encoding library - Shared libraries" }, + { "name": "qstylizer", "uri": "https://anaconda.org/anaconda/qstylizer", "description": "Qt stylesheet generation utility for PyQt/PySide" }, + { "name": "qt-main", "uri": "https://anaconda.org/anaconda/qt-main", "description": "Qt is a cross-platform application and UI framework." }, + { "name": "qt-webengine", "uri": "https://anaconda.org/anaconda/qt-webengine", "description": "Qt is a cross-platform application and UI framework." }, + { "name": "qt5compat", "uri": "https://anaconda.org/anaconda/qt5compat", "description": "Cross-platform application and UI framework (5compat libraries)." }, + { "name": "qtawesome", "uri": "https://anaconda.org/anaconda/qtawesome", "description": "Iconic fonts in PyQt and PySide applications" }, + { "name": "qtbase", "uri": "https://anaconda.org/anaconda/qtbase", "description": "Cross-platform application and UI framework (base libraries)." }, + { "name": "qtconsole", "uri": "https://anaconda.org/anaconda/qtconsole", "description": "Jupyter Qt console" }, + { "name": "qtdeclarative", "uri": "https://anaconda.org/anaconda/qtdeclarative", "description": "Cross-platform application and UI framework (declarative libraries)." }, + { "name": "qtimageformats", "uri": "https://anaconda.org/anaconda/qtimageformats", "description": "Cross-platform application and UI framework (imageformats libraries)." }, + { "name": "qtpy", "uri": "https://anaconda.org/anaconda/qtpy", "description": "Abtraction layer for PyQt5/PyQt6/PySide2/PySide6" }, + { "name": "qtshadertools", "uri": "https://anaconda.org/anaconda/qtshadertools", "description": "Cross-platform application and UI framework (shadertools libraries)." }, + { "name": "qtsvg", "uri": "https://anaconda.org/anaconda/qtsvg", "description": "Cross-platform application and UI framework (svg libraries)." }, + { "name": "qttools", "uri": "https://anaconda.org/anaconda/qttools", "description": "Cross-platform application and UI framework (tools libraries)." }, + { "name": "qttranslations", "uri": "https://anaconda.org/anaconda/qttranslations", "description": "Cross-platform application and UI framework (translations libraries)." }, + { "name": "qtwebchannel", "uri": "https://anaconda.org/anaconda/qtwebchannel", "description": "Cross-platform application and UI framework (webchannel libraries)." }, + { "name": "qtwebkit", "uri": "https://anaconda.org/anaconda/qtwebkit", "description": "WebKit is one of the major engine to render webpages and execute JavaScript code" }, + { "name": "qtwebsockets", "uri": "https://anaconda.org/anaconda/qtwebsockets", "description": "Cross-platform application and UI framework (websockets libraries)." }, + { "name": "quandl", "uri": "https://anaconda.org/anaconda/quandl", "description": "Source for financial, economic, and alternative datasets." }, + { "name": "quantecon", "uri": "https://anaconda.org/anaconda/quantecon", "description": "QuantEcon is a package to support all forms of quantitative economic modelling." }, + { "name": "querystring_parser", "uri": "https://anaconda.org/anaconda/querystring_parser", "description": "QueryString parser for Python/Django that correctly handles nested dictionaries" }, + { "name": "queuelib", "uri": "https://anaconda.org/anaconda/queuelib", "description": "Collection of persistent (disk-based) queues" }, + { "name": "quicksectx", "uri": "https://anaconda.org/anaconda/quicksectx", "description": "fast, simple interval intersection" }, + { "name": "quilt3", "uri": "https://anaconda.org/anaconda/quilt3", "description": "Quilt: where data comes together" }, + { "name": "quiver_engine", "uri": "https://anaconda.org/anaconda/quiver_engine", "description": "Interactive per-layer visualization for convents in keras" }, + { "name": "r-archive", "uri": "https://anaconda.org/anaconda/r-archive", "description": "Bindings to 'libarchive' the Multi-format archive and compression library. Offers R connections and direct extraction for many archive formats including 'tar', 'ZIP', '7-zip', 'RAR', 'CAB' and compression formats including 'gzip', 'bzip2', 'compress', 'lzma' and 'xz'." }, + { "name": "r-xgboost", "uri": "https://anaconda.org/anaconda/r-xgboost", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "r-xgboost-cpu", "uri": "https://anaconda.org/anaconda/r-xgboost-cpu", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "radon", "uri": "https://anaconda.org/anaconda/radon", "description": "Code Metrics in Python" }, + { "name": "rapidfuzz", "uri": "https://anaconda.org/anaconda/rapidfuzz", "description": "rapid fuzzy string matching" }, + { "name": "rapidjson", "uri": "https://anaconda.org/anaconda/rapidjson", "description": "A fast JSON parser/generator for C++ with both SAX/DOM style API" }, + { "name": "rasterio", "uri": "https://anaconda.org/anaconda/rasterio", "description": "Rasterio reads and writes geospatial raster datasets" }, + { "name": "rasterstats", "uri": "https://anaconda.org/anaconda/rasterstats", "description": "Summarize geospatial raster datasets based on vector geometries" }, + { "name": "ray-air", "uri": "https://anaconda.org/anaconda/ray-air", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-client", "uri": "https://anaconda.org/anaconda/ray-client", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-core", "uri": "https://anaconda.org/anaconda/ray-core", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-dashboard", "uri": "https://anaconda.org/anaconda/ray-dashboard", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-data", "uri": "https://anaconda.org/anaconda/ray-data", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-debug", "uri": "https://anaconda.org/anaconda/ray-debug", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-default", "uri": "https://anaconda.org/anaconda/ray-default", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-observability", "uri": "https://anaconda.org/anaconda/ray-observability", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-rllib", "uri": "https://anaconda.org/anaconda/ray-rllib", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-serve", "uri": "https://anaconda.org/anaconda/ray-serve", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-serve-grpc", "uri": "https://anaconda.org/anaconda/ray-serve-grpc", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-train", "uri": "https://anaconda.org/anaconda/ray-train", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "ray-tune", "uri": "https://anaconda.org/anaconda/ray-tune", "description": "Ray is a fast and simple framework for building and running distributed applications." }, + { "name": "re-assert", "uri": "https://anaconda.org/anaconda/re-assert", "description": "show where your regex match assertion failed!" }, + { "name": "re2", "uri": "https://anaconda.org/anaconda/re2", "description": "RE2 is a fast, safe, thread-friendly alternative to backtracking regular expression\nengines like those used in PCRE, Perl, and Python. It is a C++ library." }, + { "name": "readchar", "uri": "https://anaconda.org/anaconda/readchar", "description": "Python library to read characters and key strokes." }, + { "name": "readline-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/readline-amzn2-aarch64", "description": "(CDT) A library for editing typed command lines" }, + { "name": "readme_renderer", "uri": "https://anaconda.org/anaconda/readme_renderer", "description": "readme_renderer is a library for rendering readme descriptions for Warehouse" }, + { "name": "recommonmark", "uri": "https://anaconda.org/anaconda/recommonmark", "description": "A docutils-compatibility bridge to CommonMark" }, + { "name": "redis-py", "uri": "https://anaconda.org/anaconda/redis-py", "description": "Python client for Redis key-value store" }, + { "name": "referencing", "uri": "https://anaconda.org/anaconda/referencing", "description": "JSON Referencing + Python" }, + { "name": "regex", "uri": "https://anaconda.org/anaconda/regex", "description": "Alternative regular expression module, to replace re" }, + { "name": "reportlab", "uri": "https://anaconda.org/anaconda/reportlab", "description": "The Reportlab Toolkit" }, + { "name": "repoze.lru", "uri": "https://anaconda.org/anaconda/repoze.lru", "description": "A tiny LRU cache implementation and decorator" }, + { "name": "reproc", "uri": "https://anaconda.org/anaconda/reproc", "description": "reproc (Redirected Process) is a cross-platform C/C++ library that simplifies starting, stopping and communicating with external programs." }, + { "name": "reproc-cpp", "uri": "https://anaconda.org/anaconda/reproc-cpp", "description": "reproc (Redirected Process) is a cross-platform C/C++ library that simplifies starting, stopping and communicating with external programs." }, + { "name": "reproc-cpp-static", "uri": "https://anaconda.org/anaconda/reproc-cpp-static", "description": "reproc (Redirected Process) is a cross-platform C/C++ library that simplifies starting, stopping and communicating with external programs." }, + { "name": "reproc-static", "uri": "https://anaconda.org/anaconda/reproc-static", "description": "reproc (Redirected Process) is a cross-platform C/C++ library that simplifies starting, stopping and communicating with external programs." }, + { "name": "requests", "uri": "https://anaconda.org/anaconda/requests", "description": "Requests is an elegant and simple HTTP library for Python, built with ♥." }, + { "name": "requests-cache", "uri": "https://anaconda.org/anaconda/requests-cache", "description": "A transparent persistent cache for the requests library" }, + { "name": "requests-futures", "uri": "https://anaconda.org/anaconda/requests-futures", "description": "Asynchronous Python HTTP for Humans" }, + { "name": "requests-mock", "uri": "https://anaconda.org/anaconda/requests-mock", "description": "requests-mock provides a building block to stub out the HTTP requests portions of your testing code." }, + { "name": "requests-oauthlib", "uri": "https://anaconda.org/anaconda/requests-oauthlib", "description": "OAuthlib authentication support for Requests." }, + { "name": "requests-toolbelt", "uri": "https://anaconda.org/anaconda/requests-toolbelt", "description": "A toolbelt of useful classes and functions to be used with python-requests" }, + { "name": "requests-wsgi-adapter", "uri": "https://anaconda.org/anaconda/requests-wsgi-adapter", "description": "WSGI Transport Adapter for Requests" }, + { "name": "requests_download", "uri": "https://anaconda.org/anaconda/requests_download", "description": "Download files using requests and save them to a target path" }, + { "name": "requests_ntlm", "uri": "https://anaconda.org/anaconda/requests_ntlm", "description": "NTLM authentication support for Requests." }, + { "name": "resolvelib", "uri": "https://anaconda.org/anaconda/resolvelib", "description": "Simple, fast, extensible JSON encoder/decoder for Python" }, + { "name": "responses", "uri": "https://anaconda.org/anaconda/responses", "description": "A utility library for mocking out the `requests` Python library." }, + { "name": "retrying", "uri": "https://anaconda.org/anaconda/retrying", "description": "Simplify the task of adding retry behavior to just about anything." }, + { "name": "rfc3339-validator", "uri": "https://anaconda.org/anaconda/rfc3339-validator", "description": "A pure python RFC3339 validator" }, + { "name": "rfc3986", "uri": "https://anaconda.org/anaconda/rfc3986", "description": "Validating URI References per RFC 3986" }, + { "name": "rfc3986-validator", "uri": "https://anaconda.org/anaconda/rfc3986-validator", "description": "Pure python rfc3986 validator" }, + { "name": "rich", "uri": "https://anaconda.org/anaconda/rich", "description": "Rich is a Python library for rich text and beautiful formatting in the terminal." }, + { "name": "rioxarray", "uri": "https://anaconda.org/anaconda/rioxarray", "description": "rasterio xarray extension." }, + { "name": "ripgrep", "uri": "https://anaconda.org/anaconda/ripgrep", "description": "ripgrep recursively searches directories for a regex pattern while respecting your gitignore" }, + { "name": "rise", "uri": "https://anaconda.org/anaconda/rise", "description": "RISE: Live Reveal.js Jupyter/IPython Slideshow Extension" }, + { "name": "robotframework", "uri": "https://anaconda.org/anaconda/robotframework", "description": "Generic automation framework for acceptance testing and robotic process automation (RPA)" }, + { "name": "rope", "uri": "https://anaconda.org/anaconda/rope", "description": "A python refactoring library" }, + { "name": "routes", "uri": "https://anaconda.org/anaconda/routes", "description": "Routing Recognition and Generation Tools" }, + { "name": "rpds-py", "uri": "https://anaconda.org/anaconda/rpds-py", "description": "Python bindings to Rust's persistent data structures (rpds)" }, + { "name": "rsa", "uri": "https://anaconda.org/anaconda/rsa", "description": "Pure-Python RSA implementation" }, + { "name": "rstudio", "uri": "https://anaconda.org/anaconda/rstudio", "description": "A set of integrated tools designed to help you be more productive with R" }, + { "name": "rsync", "uri": "https://anaconda.org/anaconda/rsync", "description": "Tool for fast incremental file transfer" }, + { "name": "rtree", "uri": "https://anaconda.org/anaconda/rtree", "description": "R-Tree spatial index for Python GIS" }, + { "name": "ruamel", "uri": "https://anaconda.org/anaconda/ruamel", "description": "A package to ensure the `ruamel` namespace is available." }, + { "name": "ruamel.yaml", "uri": "https://anaconda.org/anaconda/ruamel.yaml", "description": "A YAML package for Python. It is a derivative of Kirill Simonov's PyYAML 3.11 which supports YAML1.1" }, + { "name": "ruamel.yaml.clib", "uri": "https://anaconda.org/anaconda/ruamel.yaml.clib", "description": "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" }, + { "name": "ruamel.yaml.jinja2", "uri": "https://anaconda.org/anaconda/ruamel.yaml.jinja2", "description": "jinja2 pre and post-processor to update YAML" }, + { "name": "ruamel_yaml", "uri": "https://anaconda.org/anaconda/ruamel_yaml", "description": "A patched copy of ruamel.yaml." }, + { "name": "ruby", "uri": "https://anaconda.org/anaconda/ruby", "description": "A dynamic, open source programming language with a focus on simplicity and productivity." }, + { "name": "ruby-cos6-x86_64", "uri": "https://anaconda.org/anaconda/ruby-cos6-x86_64", "description": "(CDT) An interpreter of object-oriented scripting language" }, + { "name": "ruby-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/ruby-cos7-ppc64le", "description": "(CDT) An interpreter of object-oriented scripting language" }, + { "name": "ruby-libs-cos6-x86_64", "uri": "https://anaconda.org/anaconda/ruby-libs-cos6-x86_64", "description": "(CDT) Libraries necessary to run Ruby" }, + { "name": "ruby-libs-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/ruby-libs-cos7-ppc64le", "description": "(CDT) Libraries necessary to run Ruby" }, + { "name": "rubygem-json-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/rubygem-json-cos7-ppc64le", "description": "(CDT) This is a JSON implementation as a Ruby extension in C" }, + { "name": "ruff", "uri": "https://anaconda.org/anaconda/ruff", "description": "An extremely fast Python linter, written in Rust." }, + { "name": "rust", "uri": "https://anaconda.org/anaconda/rust", "description": "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety." }, + { "name": "rust-gnu", "uri": "https://anaconda.org/anaconda/rust-gnu", "description": "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety." }, + { "name": "rust-gnu_win-32", "uri": "https://anaconda.org/anaconda/rust-gnu_win-32", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust-gnu_win-64", "uri": "https://anaconda.org/anaconda/rust-gnu_win-64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust-nightly", "uri": "https://anaconda.org/anaconda/rust-nightly", "description": "Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.\nThis package provides the compiler (rustc) and the documentation utilities rustdoc." }, + { "name": "rust-nightly_linux-64", "uri": "https://anaconda.org/anaconda/rust-nightly_linux-64", "description": "A safe systems programming language" }, + { "name": "rust-nightly_osx-64", "uri": "https://anaconda.org/anaconda/rust-nightly_osx-64", "description": "A safe systems programming language" }, + { "name": "rust_linux-64", "uri": "https://anaconda.org/anaconda/rust_linux-64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_linux-aarch64", "uri": "https://anaconda.org/anaconda/rust_linux-aarch64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_linux-ppc64le", "uri": "https://anaconda.org/anaconda/rust_linux-ppc64le", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_linux-s390x", "uri": "https://anaconda.org/anaconda/rust_linux-s390x", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_osx-64", "uri": "https://anaconda.org/anaconda/rust_osx-64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_osx-arm64", "uri": "https://anaconda.org/anaconda/rust_osx-arm64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_win-32", "uri": "https://anaconda.org/anaconda/rust_win-32", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "rust_win-64", "uri": "https://anaconda.org/anaconda/rust_win-64", "description": "A safe systems programming language (conda activation scripts)" }, + { "name": "s2n", "uri": "https://anaconda.org/anaconda/s2n", "description": "an implementation of the TLS/SSL protocols" }, + { "name": "s2n-static", "uri": "https://anaconda.org/anaconda/s2n-static", "description": "an implementation of the TLS/SSL protocols" }, + { "name": "s3fs", "uri": "https://anaconda.org/anaconda/s3fs", "description": "Convenient Filesystem interface over S3" }, + { "name": "s3transfer", "uri": "https://anaconda.org/anaconda/s3transfer", "description": "An Amazon S3 Transfer Manager for Python" }, + { "name": "sacrebleu", "uri": "https://anaconda.org/anaconda/sacrebleu", "description": "Reference BLEU implementation that auto-downloads test sets and reports a version string to facilitate cross-lab comparisons" }, + { "name": "sacremoses", "uri": "https://anaconda.org/anaconda/sacremoses", "description": "SacreMoses" }, + { "name": "safeint", "uri": "https://anaconda.org/anaconda/safeint", "description": "SafeInt is a class library for C++ that manages integer overflows." }, + { "name": "safetensors", "uri": "https://anaconda.org/anaconda/safetensors", "description": "Fast and Safe Tensor serialization" }, + { "name": "salib", "uri": "https://anaconda.org/anaconda/salib", "description": "Sensitivity Analysis Library" }, + { "name": "salt", "uri": "https://anaconda.org/anaconda/salt", "description": "Software to automate the management and configuration of any infrastructure or application at scale" }, + { "name": "sarif_om", "uri": "https://anaconda.org/anaconda/sarif_om", "description": "Classes implementing the SARIF 2.1.0 object model." }, + { "name": "sas_kernel", "uri": "https://anaconda.org/anaconda/sas_kernel", "description": "A Jupyter kernel for SAS" }, + { "name": "saspy", "uri": "https://anaconda.org/anaconda/saspy", "description": "A Python interface module to the SAS System" }, + { "name": "scandir", "uri": "https://anaconda.org/anaconda/scandir", "description": "scandir, a better directory iterator and faster os.walk()" }, + { "name": "scapy", "uri": "https://anaconda.org/anaconda/scapy", "description": "Scapy: interactive packet manipulation tool" }, + { "name": "schema", "uri": "https://anaconda.org/anaconda/schema", "description": "Schema validation just got Pythonic" }, + { "name": "schemdraw", "uri": "https://anaconda.org/anaconda/schemdraw", "description": "Electrical circuit schematic drawing" }, + { "name": "scikit-build", "uri": "https://anaconda.org/anaconda/scikit-build", "description": "Improved build system generator for CPython C extensions." }, + { "name": "scikit-build-core", "uri": "https://anaconda.org/anaconda/scikit-build-core", "description": "Build backend for CMake based projects" }, + { "name": "scikit-image", "uri": "https://anaconda.org/anaconda/scikit-image", "description": "Image processing in Python." }, + { "name": "scikit-learn", "uri": "https://anaconda.org/anaconda/scikit-learn", "description": "A set of python modules for machine learning and data mining" }, + { "name": "scikit-learn-intelex", "uri": "https://anaconda.org/anaconda/scikit-learn-intelex", "description": "Intel(R) Extension for Scikit-learn is a seamless way to speed up your Scikit-learn application." }, + { "name": "scikit-plot", "uri": "https://anaconda.org/anaconda/scikit-plot", "description": "An intuitive library to add plotting functionality to scikit-learn objects." }, + { "name": "scikit-rf", "uri": "https://anaconda.org/anaconda/scikit-rf", "description": "Object Oriented Microwave Engineering." }, + { "name": "scipy", "uri": "https://anaconda.org/anaconda/scipy", "description": "Scientific Library for Python" }, + { "name": "scons", "uri": "https://anaconda.org/anaconda/scons", "description": "Open Source next-generation build tool." }, + { "name": "scotch", "uri": "https://anaconda.org/anaconda/scotch", "description": "SCOTCH: Static Mapping, Graph, Mesh and Hypergraph Partitioning, and Parallel and Sequential Sparse Matrix Ordering Package" }, + { "name": "scp", "uri": "https://anaconda.org/anaconda/scp", "description": "Pure python scp module for paramiko" }, + { "name": "scramp", "uri": "https://anaconda.org/anaconda/scramp", "description": "A pure-Python implementation of the SCRAM authentication protocol" }, + { "name": "scrapy", "uri": "https://anaconda.org/anaconda/scrapy", "description": "A high-level Python Screen Scraping framework" }, + { "name": "scs", "uri": "https://anaconda.org/anaconda/scs", "description": "Python interface for SCS, which solves convex cone problems" }, + { "name": "seaborn", "uri": "https://anaconda.org/anaconda/seaborn", "description": "Statistical data visualization" }, + { "name": "secretstorage", "uri": "https://anaconda.org/anaconda/secretstorage", "description": "Provides a way for securely storing passwords and other secrets." }, + { "name": "sed", "uri": "https://anaconda.org/anaconda/sed", "description": "sed (stream editor) is a non-interactive command-line text editor." }, + { "name": "sed-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/sed-amzn2-aarch64", "description": "(CDT) A GNU stream text editor" }, + { "name": "segregation", "uri": "https://anaconda.org/anaconda/segregation", "description": "Segregation Measures Framework in PySAL" }, + { "name": "selectors2", "uri": "https://anaconda.org/anaconda/selectors2", "description": "Back-ported, durable, and portable selectors" }, + { "name": "selenium", "uri": "https://anaconda.org/anaconda/selenium", "description": "Python bindings for the Selenium WebDriver for automating web browser interaction." }, + { "name": "semver", "uri": "https://anaconda.org/anaconda/semver", "description": "Python package to work with Semantic Versioning" }, + { "name": "send2trash", "uri": "https://anaconda.org/anaconda/send2trash", "description": "Python library to natively send files to Trash (or Recycle bin) on all platforms." }, + { "name": "sendgrid", "uri": "https://anaconda.org/anaconda/sendgrid", "description": "SendGrid library for Python" }, + { "name": "sentencepiece", "uri": "https://anaconda.org/anaconda/sentencepiece", "description": "Unsupervised text tokenizer for Neural Network-based text generation." }, + { "name": "sentencepiece-python", "uri": "https://anaconda.org/anaconda/sentencepiece-python", "description": "Unsupervised text tokenizer for Neural Network-based text generation." }, + { "name": "sentencepiece-spm", "uri": "https://anaconda.org/anaconda/sentencepiece-spm", "description": "Unsupervised text tokenizer for Neural Network-based text generation." }, + { "name": "sentry-sdk", "uri": "https://anaconda.org/anaconda/sentry-sdk", "description": "The new Python SDK for Sentry.io" }, + { "name": "serf", "uri": "https://anaconda.org/anaconda/serf", "description": "High performance C-based HTTP client library" }, + { "name": "serverfiles", "uri": "https://anaconda.org/anaconda/serverfiles", "description": "An utility that accesses files on a HTTP server and stores them locally for reuse" }, + { "name": "setproctitle", "uri": "https://anaconda.org/anaconda/setproctitle", "description": "A library to allow customization of the process title." }, + { "name": "setup-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/setup-amzn2-aarch64", "description": "(CDT) A set of system configuration and setup files" }, + { "name": "setuptools", "uri": "https://anaconda.org/anaconda/setuptools", "description": "Download, build, install, upgrade, and uninstall Python packages" }, + { "name": "setuptools-git", "uri": "https://anaconda.org/anaconda/setuptools-git", "description": "Setuptools revision control system plugin for Git" }, + { "name": "setuptools-git-versioning", "uri": "https://anaconda.org/anaconda/setuptools-git-versioning", "description": "Use git repo data for building a version number according PEP-440" }, + { "name": "setuptools-rust", "uri": "https://anaconda.org/anaconda/setuptools-rust", "description": "Setuptools rust extension plugin" }, + { "name": "setuptools-scm", "uri": "https://anaconda.org/anaconda/setuptools-scm", "description": "The blessed package to manage your versions by scm tags" }, + { "name": "setuptools-scm-git-archive", "uri": "https://anaconda.org/anaconda/setuptools-scm-git-archive", "description": "setuptools_scm plugin for git archives" }, + { "name": "setuptools_scm", "uri": "https://anaconda.org/anaconda/setuptools_scm", "description": "The blessed package to manage your versions by scm tags" }, + { "name": "setuptools_scm_git_archive", "uri": "https://anaconda.org/anaconda/setuptools_scm_git_archive", "description": "setuptools_scm plugin for git archives" }, + { "name": "sgmllib3k", "uri": "https://anaconda.org/anaconda/sgmllib3k", "description": "Py3k port of sgmllib." }, + { "name": "sh", "uri": "https://anaconda.org/anaconda/sh", "description": "Python subprocess interface" }, + { "name": "shadow-utils-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/shadow-utils-amzn2-aarch64", "description": "(CDT) Utilities for managing accounts and shadow password files" }, + { "name": "shap", "uri": "https://anaconda.org/anaconda/shap", "description": "A unified approach to explain the output of any machine learning model." }, + { "name": "shapely", "uri": "https://anaconda.org/anaconda/shapely", "description": "Python package for manipulation and analysis of geometric objects in the Cartesian plane" }, + { "name": "shared-mime-info-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/shared-mime-info-amzn2-aarch64", "description": "(CDT) Shared MIME information database" }, + { "name": "shellcheck", "uri": "https://anaconda.org/anaconda/shellcheck", "description": "ShellCheck, a static analysis tool for shell scripts" }, + { "name": "shellingham", "uri": "https://anaconda.org/anaconda/shellingham", "description": "Tool to Detect Surrounding Shell" }, + { "name": "simdjson", "uri": "https://anaconda.org/anaconda/simdjson", "description": "Parsing gigabytes of JSON per second" }, + { "name": "simdjson-static", "uri": "https://anaconda.org/anaconda/simdjson-static", "description": "Parsing gigabytes of JSON per second" }, + { "name": "simpervisor", "uri": "https://anaconda.org/anaconda/simpervisor", "description": "Simple async process supervisor" }, + { "name": "simple-salesforce", "uri": "https://anaconda.org/anaconda/simple-salesforce", "description": "Simple Salesforce is a basic Salesforce.com REST API client. The goal is to provide a very low-level interface to the API, returning an ordered dictionary of the API JSON response." }, + { "name": "simplejson", "uri": "https://anaconda.org/anaconda/simplejson", "description": "Simple, fast, extensible JSON encoder/decoder for Python" }, + { "name": "sip", "uri": "https://anaconda.org/anaconda/sip", "description": "A Python bindings generator for C/C++ libraries" }, + { "name": "skl2onnx", "uri": "https://anaconda.org/anaconda/skl2onnx", "description": "Convert scikit-learn models to ONNX" }, + { "name": "sklearn-pandas", "uri": "https://anaconda.org/anaconda/sklearn-pandas", "description": "Pandas integration with sklearn" }, + { "name": "slack-sdk", "uri": "https://anaconda.org/anaconda/slack-sdk", "description": "The Slack API Platform SDK for Python" }, + { "name": "slack_sdk", "uri": "https://anaconda.org/anaconda/slack_sdk", "description": "The Slack API Platform SDK for Python" }, + { "name": "slackclient", "uri": "https://anaconda.org/anaconda/slackclient", "description": "Python client for Slack.com" }, + { "name": "sleef", "uri": "https://anaconda.org/anaconda/sleef", "description": "SIMD library for evaluating elementary functions" }, + { "name": "slicer", "uri": "https://anaconda.org/anaconda/slicer", "description": "Unified slicing for all Python data structures." }, + { "name": "slicerator", "uri": "https://anaconda.org/anaconda/slicerator", "description": "A lazy-loading, fancy-sliceable iterable." }, + { "name": "smart_open", "uri": "https://anaconda.org/anaconda/smart_open", "description": "Python library for efficient streaming of large files" }, + { "name": "smartypants", "uri": "https://anaconda.org/anaconda/smartypants", "description": "Python with the SmartyPants" }, + { "name": "smmap", "uri": "https://anaconda.org/anaconda/smmap", "description": "A pure git implementation of a sliding window memory map manager." }, + { "name": "snakebite-py3", "uri": "https://anaconda.org/anaconda/snakebite-py3", "description": "Pure Python HDFS client" }, + { "name": "snakeviz", "uri": "https://anaconda.org/anaconda/snakeviz", "description": "Web-based viewer for Python profiler output" }, + { "name": "snappy", "uri": "https://anaconda.org/anaconda/snappy", "description": "A fast compressor/decompressor" }, + { "name": "sniffio", "uri": "https://anaconda.org/anaconda/sniffio", "description": "Sniff out which async library your code is running under" }, + { "name": "snowflake-connector-python", "uri": "https://anaconda.org/anaconda/snowflake-connector-python", "description": "Snowflake Connector for Python" }, + { "name": "snowflake-ml-python", "uri": "https://anaconda.org/anaconda/snowflake-ml-python", "description": "Snowflake machine learning library." }, + { "name": "snowflake-snowpark-python", "uri": "https://anaconda.org/anaconda/snowflake-snowpark-python", "description": "Snowflake Snowpark Python API" }, + { "name": "soappy", "uri": "https://anaconda.org/anaconda/soappy", "description": "Simple to use SOAP library for Python" }, + { "name": "sortedcontainers", "uri": "https://anaconda.org/anaconda/sortedcontainers", "description": "Python Sorted Container Types: SortedList, SortedDict, and SortedSet" }, + { "name": "soupsieve", "uri": "https://anaconda.org/anaconda/soupsieve", "description": "A modern CSS selector implementation for BeautifulSoup" }, + { "name": "spacy", "uri": "https://anaconda.org/anaconda/spacy", "description": "Industrial-strength Natural Language Processing (NLP) in Python" }, + { "name": "spacy-alignments", "uri": "https://anaconda.org/anaconda/spacy-alignments", "description": "Align tokenizations for spaCy + transformers" }, + { "name": "spacy-legacy", "uri": "https://anaconda.org/anaconda/spacy-legacy", "description": "Legacy functions and architectures for backwards compatibility" }, + { "name": "spacy-loggers", "uri": "https://anaconda.org/anaconda/spacy-loggers", "description": "Alternate loggers for spaCy pipeline training" }, + { "name": "spacy-model-ca_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-ca_core_news_lg", "description": "Catalan pipeline optimized for CPU." }, + { "name": "spacy-model-ca_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-ca_core_news_md", "description": "Catalan pipeline optimized for CPU." }, + { "name": "spacy-model-ca_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-ca_core_news_sm", "description": "Catalan pipeline optimized for CPU." }, + { "name": "spacy-model-ca_core_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-ca_core_news_trf", "description": "Catalan transformer pipeline (projecte-aina/roberta-base-ca-v2)." }, + { "name": "spacy-model-da_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-da_core_news_lg", "description": "Danish pipeline optimized for CPU." }, + { "name": "spacy-model-da_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-da_core_news_md", "description": "Danish pipeline optimized for CPU." }, + { "name": "spacy-model-da_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-da_core_news_sm", "description": "Danish pipeline optimized for CPU." }, + { "name": "spacy-model-da_core_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-da_core_news_trf", "description": "Danish transformer pipeline (Maltehb/danish-bert-botxo)." }, + { "name": "spacy-model-de_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-de_core_news_lg", "description": "German pipeline optimized for CPU." }, + { "name": "spacy-model-de_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-de_core_news_md", "description": "German pipeline optimized for CPU." }, + { "name": "spacy-model-de_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-de_core_news_sm", "description": "German pipeline optimized for CPU." }, + { "name": "spacy-model-de_dep_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-de_dep_news_trf", "description": "German transformer pipeline (bert-base-german-cased)." }, + { "name": "spacy-model-el_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-el_core_news_lg", "description": "Greek pipeline optimized for CPU." }, + { "name": "spacy-model-el_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-el_core_news_md", "description": "Greek pipeline optimized for CPU." }, + { "name": "spacy-model-el_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-el_core_news_sm", "description": "Greek pipeline optimized for CPU." }, + { "name": "spacy-model-en_core_web_lg", "uri": "https://anaconda.org/anaconda/spacy-model-en_core_web_lg", "description": "English pipeline optimized for CPU." }, + { "name": "spacy-model-en_core_web_md", "uri": "https://anaconda.org/anaconda/spacy-model-en_core_web_md", "description": "English pipeline optimized for CPU." }, + { "name": "spacy-model-en_core_web_sm", "uri": "https://anaconda.org/anaconda/spacy-model-en_core_web_sm", "description": "English pipeline optimized for CPU." }, + { "name": "spacy-model-en_core_web_trf", "uri": "https://anaconda.org/anaconda/spacy-model-en_core_web_trf", "description": "English transformer pipeline (roberta-base)." }, + { "name": "spacy-model-es_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-es_core_news_lg", "description": "Spanish pipeline optimized for CPU." }, + { "name": "spacy-model-es_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-es_core_news_md", "description": "Spanish pipeline optimized for CPU." }, + { "name": "spacy-model-es_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-es_core_news_sm", "description": "Spanish pipeline optimized for CPU." }, + { "name": "spacy-model-es_dep_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-es_dep_news_trf", "description": "Spanish transformer pipeline (dccuchile/bert-base-spanish-wwm-cased)." }, + { "name": "spacy-model-fi_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-fi_core_news_lg", "description": "Finnish pipeline optimized for CPU." }, + { "name": "spacy-model-fi_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-fi_core_news_md", "description": "Finnish pipeline optimized for CPU." }, + { "name": "spacy-model-fi_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-fi_core_news_sm", "description": "Finnish pipeline optimized for CPU." }, + { "name": "spacy-model-fr_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-fr_core_news_lg", "description": "French pipeline optimized for CPU." }, + { "name": "spacy-model-fr_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-fr_core_news_md", "description": "French pipeline optimized for CPU." }, + { "name": "spacy-model-fr_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-fr_core_news_sm", "description": "French pipeline optimized for CPU." }, + { "name": "spacy-model-fr_dep_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-fr_dep_news_trf", "description": "French transformer pipeline (camembert-base)." }, + { "name": "spacy-model-hr_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-hr_core_news_lg", "description": "Croatian pipeline optimized for CPU." }, + { "name": "spacy-model-hr_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-hr_core_news_md", "description": "Croatian pipeline optimized for CPU." }, + { "name": "spacy-model-hr_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-hr_core_news_sm", "description": "Croatian pipeline optimized for CPU." }, + { "name": "spacy-model-it_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-it_core_news_lg", "description": "Italian pipeline optimized for CPU." }, + { "name": "spacy-model-it_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-it_core_news_md", "description": "Italian pipeline optimized for CPU." }, + { "name": "spacy-model-it_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-it_core_news_sm", "description": "Italian pipeline optimized for CPU." }, + { "name": "spacy-model-ja_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-ja_core_news_lg", "description": "Japanese pipeline optimized for CPU." }, + { "name": "spacy-model-ja_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-ja_core_news_md", "description": "Japanese pipeline optimized for CPU." }, + { "name": "spacy-model-ja_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-ja_core_news_sm", "description": "Japanese pipeline optimized for CPU." }, + { "name": "spacy-model-ja_core_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-ja_core_news_trf", "description": "Japanese transformer pipeline (cl-tohoku/bert-base-japanese-char-v2)." }, + { "name": "spacy-model-ko_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-ko_core_news_lg", "description": "Korean pipeline optimized for CPU." }, + { "name": "spacy-model-ko_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-ko_core_news_md", "description": "Korean pipeline optimized for CPU." }, + { "name": "spacy-model-ko_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-ko_core_news_sm", "description": "Korean pipeline optimized for CPU." }, + { "name": "spacy-model-lt_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-lt_core_news_lg", "description": "Lithuanian pipeline optimized for CPU." }, + { "name": "spacy-model-lt_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-lt_core_news_md", "description": "Lithuanian pipeline optimized for CPU." }, + { "name": "spacy-model-lt_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-lt_core_news_sm", "description": "Lithuanian pipeline optimized for CPU." }, + { "name": "spacy-model-mk_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-mk_core_news_lg", "description": "Macedonian pipeline optimized for CPU." }, + { "name": "spacy-model-mk_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-mk_core_news_md", "description": "Macedonian pipeline optimized for CPU." }, + { "name": "spacy-model-mk_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-mk_core_news_sm", "description": "Macedonian pipeline optimized for CPU." }, + { "name": "spacy-model-nb_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-nb_core_news_lg", "description": "Norwegian (Bokmål) pipeline optimized for CPU." }, + { "name": "spacy-model-nb_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-nb_core_news_md", "description": "Norwegian (Bokmål) pipeline optimized for CPU." }, + { "name": "spacy-model-nb_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-nb_core_news_sm", "description": "Norwegian (Bokmål) pipeline optimized for CPU." }, + { "name": "spacy-model-nl_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-nl_core_news_lg", "description": "Dutch pipeline optimized for CPU." }, + { "name": "spacy-model-nl_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-nl_core_news_md", "description": "Dutch pipeline optimized for CPU." }, + { "name": "spacy-model-nl_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-nl_core_news_sm", "description": "Dutch pipeline optimized for CPU." }, + { "name": "spacy-model-pl_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-pl_core_news_lg", "description": "Polish pipeline optimized for CPU." }, + { "name": "spacy-model-pl_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-pl_core_news_md", "description": "Polish pipeline optimized for CPU." }, + { "name": "spacy-model-pl_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-pl_core_news_sm", "description": "Polish pipeline optimized for CPU." }, + { "name": "spacy-model-pt_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-pt_core_news_lg", "description": "Portuguese pipeline optimized for CPU." }, + { "name": "spacy-model-pt_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-pt_core_news_md", "description": "Portuguese pipeline optimized for CPU." }, + { "name": "spacy-model-pt_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-pt_core_news_sm", "description": "Portuguese pipeline optimized for CPU." }, + { "name": "spacy-model-ro_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-ro_core_news_lg", "description": "Romanian pipeline optimized for CPU." }, + { "name": "spacy-model-ro_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-ro_core_news_md", "description": "Romanian pipeline optimized for CPU." }, + { "name": "spacy-model-ro_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-ro_core_news_sm", "description": "Romanian pipeline optimized for CPU." }, + { "name": "spacy-model-ru_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-ru_core_news_lg", "description": "Russian pipeline optimized for CPU." }, + { "name": "spacy-model-ru_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-ru_core_news_md", "description": "Russian pipeline optimized for CPU." }, + { "name": "spacy-model-ru_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-ru_core_news_sm", "description": "Russian pipeline optimized for CPU." }, + { "name": "spacy-model-sl_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-sl_core_news_lg", "description": "Slovenian pipeline optimized for CPU." }, + { "name": "spacy-model-sl_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-sl_core_news_md", "description": "Slovenian pipeline optimized for CPU." }, + { "name": "spacy-model-sl_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-sl_core_news_sm", "description": "Slovenian pipeline optimized for CPU." }, + { "name": "spacy-model-sv_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-sv_core_news_lg", "description": "Swedish pipeline optimized for CPU." }, + { "name": "spacy-model-sv_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-sv_core_news_md", "description": "Swedish pipeline optimized for CPU." }, + { "name": "spacy-model-sv_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-sv_core_news_sm", "description": "Swedish pipeline optimized for CPU." }, + { "name": "spacy-model-uk_core_news_lg", "uri": "https://anaconda.org/anaconda/spacy-model-uk_core_news_lg", "description": "Ukrainian pipeline optimized for CPU." }, + { "name": "spacy-model-uk_core_news_md", "uri": "https://anaconda.org/anaconda/spacy-model-uk_core_news_md", "description": "Ukrainian pipeline optimized for CPU." }, + { "name": "spacy-model-uk_core_news_sm", "uri": "https://anaconda.org/anaconda/spacy-model-uk_core_news_sm", "description": "Ukrainian pipeline optimized for CPU." }, + { "name": "spacy-model-uk_core_news_trf", "uri": "https://anaconda.org/anaconda/spacy-model-uk_core_news_trf", "description": "Ukrainian transformer pipeline (ukr-models/xlm-roberta-base-uk)." }, + { "name": "spacy-model-xx_ent_wiki_sm", "uri": "https://anaconda.org/anaconda/spacy-model-xx_ent_wiki_sm", "description": "Multi-language pipeline optimized for CPU." }, + { "name": "spacy-model-xx_sent_ud_sm", "uri": "https://anaconda.org/anaconda/spacy-model-xx_sent_ud_sm", "description": "Multi-language pipeline optimized for CPU." }, + { "name": "spacy-model-zh_core_web_lg", "uri": "https://anaconda.org/anaconda/spacy-model-zh_core_web_lg", "description": "Chinese pipeline optimized for CPU." }, + { "name": "spacy-model-zh_core_web_md", "uri": "https://anaconda.org/anaconda/spacy-model-zh_core_web_md", "description": "Chinese pipeline optimized for CPU." }, + { "name": "spacy-model-zh_core_web_sm", "uri": "https://anaconda.org/anaconda/spacy-model-zh_core_web_sm", "description": "Chinese pipeline optimized for CPU." }, + { "name": "spacy-model-zh_core_web_trf", "uri": "https://anaconda.org/anaconda/spacy-model-zh_core_web_trf", "description": "Chinese transformer pipeline (bert-base-chinese)." }, + { "name": "spacy-pkuseg", "uri": "https://anaconda.org/anaconda/spacy-pkuseg", "description": "PKUSeg Chinese word segmentation toolkit for spaCy" }, + { "name": "spacy-transformers", "uri": "https://anaconda.org/anaconda/spacy-transformers", "description": "Use pretrained transformers like BERT, XLNet and GPT-2 in spaCy" }, + { "name": "spaghetti", "uri": "https://anaconda.org/anaconda/spaghetti", "description": "SPAtial GrapHs: nETworks, Topology, & Inference" }, + { "name": "spark-nlp", "uri": "https://anaconda.org/anaconda/spark-nlp", "description": "John Snow Labs Spark NLP is a natural language processing library built on top of Apache Spark ML" }, + { "name": "sparkmagic", "uri": "https://anaconda.org/anaconda/sparkmagic", "description": "Jupyter magics and kernels for working with remote Spark clusters" }, + { "name": "spatialpandas", "uri": "https://anaconda.org/anaconda/spatialpandas", "description": "Pandas extension arrays for spatial/geometric operations" }, + { "name": "spdlog", "uri": "https://anaconda.org/anaconda/spdlog", "description": "Super fast C++ logging library." }, + { "name": "spdlog-fmt-embed", "uri": "https://anaconda.org/anaconda/spdlog-fmt-embed", "description": "Super fast C++ logging library." }, + { "name": "spglm", "uri": "https://anaconda.org/anaconda/spglm", "description": "Sparse generalized linear models" }, + { "name": "sphinx", "uri": "https://anaconda.org/anaconda/sphinx", "description": "Sphinx is a tool that makes it easy to create intelligent and beautiful documentation" }, + { "name": "sphinx-design", "uri": "https://anaconda.org/anaconda/sphinx-design", "description": "A sphinx extension for designing beautiful, screen-size responsive web components" }, + { "name": "sphinx-rtd-theme", "uri": "https://anaconda.org/anaconda/sphinx-rtd-theme", "description": "Sphinx theme for readthedocs.org" }, + { "name": "sphinx-sitemap", "uri": "https://anaconda.org/anaconda/sphinx-sitemap", "description": "Sitemap generator for Sphinx" }, + { "name": "sphinx_rtd_theme", "uri": "https://anaconda.org/anaconda/sphinx_rtd_theme", "description": "Sphinx theme for readthedocs.org" }, + { "name": "sphinxcontrib", "uri": "https://anaconda.org/anaconda/sphinxcontrib", "description": "Python namespace for sphinxcontrib" }, + { "name": "sphinxcontrib-applehelp", "uri": "https://anaconda.org/anaconda/sphinxcontrib-applehelp", "description": "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" }, + { "name": "sphinxcontrib-devhelp", "uri": "https://anaconda.org/anaconda/sphinxcontrib-devhelp", "description": "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document" }, + { "name": "sphinxcontrib-htmlhelp", "uri": "https://anaconda.org/anaconda/sphinxcontrib-htmlhelp", "description": "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files." }, + { "name": "sphinxcontrib-images", "uri": "https://anaconda.org/anaconda/sphinxcontrib-images", "description": "Sphinx extension for thumbnails" }, + { "name": "sphinxcontrib-jquery", "uri": "https://anaconda.org/anaconda/sphinxcontrib-jquery", "description": "A sphinx extension to include jQuery on newer sphinx releases" }, + { "name": "sphinxcontrib-jsmath", "uri": "https://anaconda.org/anaconda/sphinxcontrib-jsmath", "description": "A sphinx extension which renders display math in HTML via JavaScript" }, + { "name": "sphinxcontrib-qthelp", "uri": "https://anaconda.org/anaconda/sphinxcontrib-qthelp", "description": "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document" }, + { "name": "sphinxcontrib-serializinghtml", "uri": "https://anaconda.org/anaconda/sphinxcontrib-serializinghtml", "description": "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." }, + { "name": "sphinxcontrib-websupport", "uri": "https://anaconda.org/anaconda/sphinxcontrib-websupport", "description": "Sphinx API for Web Apps" }, + { "name": "sphinxcontrib-youtube", "uri": "https://anaconda.org/anaconda/sphinxcontrib-youtube", "description": "Sphinx \"youtube\" extension" }, + { "name": "spin", "uri": "https://anaconda.org/anaconda/spin", "description": "Developer tool for scientific Python libraries" }, + { "name": "spint", "uri": "https://anaconda.org/anaconda/spint", "description": "Efficient calibration of spatial interaction models in Python" }, + { "name": "spirv-tools", "uri": "https://anaconda.org/anaconda/spirv-tools", "description": "The SPIR-V Tools project provides an API and commands for processing SPIR-V modules." }, + { "name": "splot", "uri": "https://anaconda.org/anaconda/splot", "description": "Lightweight plotting and mapping to facilitate spatial analysis with PySAL" }, + { "name": "splunk-opentelemetry", "uri": "https://anaconda.org/anaconda/splunk-opentelemetry", "description": "The Splunk distribution of OpenTelemetry Python Instrumentation provides a Python agent that automatically instruments your Python application to capture and report distributed traces to SignalFx APM." }, + { "name": "spreg", "uri": "https://anaconda.org/anaconda/spreg", "description": "PySAL Spatial Econometrics Package" }, + { "name": "spvcm", "uri": "https://anaconda.org/anaconda/spvcm", "description": "Gibbs sampling for spatially-correlated variance-components" }, + { "name": "spyder", "uri": "https://anaconda.org/anaconda/spyder", "description": "The Scientific Python Development Environment" }, + { "name": "spyder-kernels", "uri": "https://anaconda.org/anaconda/spyder-kernels", "description": "Jupyter kernels for Spyder's console" }, + { "name": "spyder_parso_requirement", "uri": "https://anaconda.org/anaconda/spyder_parso_requirement", "description": "Ancillary package to ensure the Spyder parso requirement is in current_repodata.json" }, + { "name": "sqlalchemy", "uri": "https://anaconda.org/anaconda/sqlalchemy", "description": "Database Abstraction Library." }, + { "name": "sqlalchemy-jsonfield", "uri": "https://anaconda.org/anaconda/sqlalchemy-jsonfield", "description": "SQLALchemy JSONField implementation for storing dicts at SQL independently\nfrom JSON type support." }, + { "name": "sqlalchemy-utils", "uri": "https://anaconda.org/anaconda/sqlalchemy-utils", "description": "Various utility functions for SQLAlchemy" }, + { "name": "sqlglot", "uri": "https://anaconda.org/anaconda/sqlglot", "description": "An easily customizable SQL parser and transpiler" }, + { "name": "sqlite", "uri": "https://anaconda.org/anaconda/sqlite", "description": "Implements a self-contained, zero-configuration, SQL database engine" }, + { "name": "sqlparse", "uri": "https://anaconda.org/anaconda/sqlparse", "description": "A non-validating SQL parser module for Python." }, + { "name": "squarify", "uri": "https://anaconda.org/anaconda/squarify", "description": "Pure Python implementation of the squarify treemap layout algorithm." }, + { "name": "srsly", "uri": "https://anaconda.org/anaconda/srsly", "description": "Modern high-performance serialization utilities for Python" }, + { "name": "sshpubkeys", "uri": "https://anaconda.org/anaconda/sshpubkeys", "description": "SSH public key parser" }, + { "name": "sshtunnel", "uri": "https://anaconda.org/anaconda/sshtunnel", "description": "Pure Python SSH tunnels" }, + { "name": "st-annotated-text", "uri": "https://anaconda.org/anaconda/st-annotated-text", "description": "A simple component to display annotated text in Streamlit apps." }, + { "name": "stack_data", "uri": "https://anaconda.org/anaconda/stack_data", "description": "Extract data from python stack frames and tracebacks for informative displays" }, + { "name": "starkbank-ecdsa", "uri": "https://anaconda.org/anaconda/starkbank-ecdsa", "description": "A lightweight and fast pure python ECDSA library" }, + { "name": "starlette", "uri": "https://anaconda.org/anaconda/starlette", "description": "The little ASGI framework that shines" }, + { "name": "starlette-full", "uri": "https://anaconda.org/anaconda/starlette-full", "description": "The little ASGI framework that shines" }, + { "name": "starsessions", "uri": "https://anaconda.org/anaconda/starsessions", "description": "Pluggable session support for Starlette." }, + { "name": "statistics", "uri": "https://anaconda.org/anaconda/statistics", "description": "A Python 2.* port of 3.4 Statistics Module" }, + { "name": "statsd", "uri": "https://anaconda.org/anaconda/statsd", "description": "A Python client for statsd" }, + { "name": "statsmodels", "uri": "https://anaconda.org/anaconda/statsmodels", "description": "Statistical computations and models for use with SciPy" }, + { "name": "stdlib-list", "uri": "https://anaconda.org/anaconda/stdlib-list", "description": "A list of standard libraries for Python 2.6 through 3.12." }, + { "name": "stone", "uri": "https://anaconda.org/anaconda/stone", "description": "Stone is an interface description language (IDL) for APIs." }, + { "name": "strace_linux-64", "uri": "https://anaconda.org/anaconda/strace_linux-64", "description": "Strace is a linux diagnostic, and debugging utility with cli" }, + { "name": "strace_linux-aarch64", "uri": "https://anaconda.org/anaconda/strace_linux-aarch64", "description": "Strace is a linux diagnostic, and debugging utility with cli" }, + { "name": "strace_linux-ppc64le", "uri": "https://anaconda.org/anaconda/strace_linux-ppc64le", "description": "Strace is a linux diagnostic, and debugging utility with cli" }, + { "name": "strace_linux-s390x", "uri": "https://anaconda.org/anaconda/strace_linux-s390x", "description": "Strace is a linux diagnostic, and debugging utility with cli" }, + { "name": "streamlit", "uri": "https://anaconda.org/anaconda/streamlit", "description": "The fastest way to build data apps in Python" }, + { "name": "streamlit-camera-input-live", "uri": "https://anaconda.org/anaconda/streamlit-camera-input-live", "description": "Alternative version of st.camera_input which returns the webcam images live, without any button press needed" }, + { "name": "streamlit-card", "uri": "https://anaconda.org/anaconda/streamlit-card", "description": "A streamlit component, to make UI cards" }, + { "name": "streamlit-chat", "uri": "https://anaconda.org/anaconda/streamlit-chat", "description": "A streamlit component, to make chatbots" }, + { "name": "streamlit-embedcode", "uri": "https://anaconda.org/anaconda/streamlit-embedcode", "description": "Streamlit component for embedded code snippets" }, + { "name": "streamlit-extras", "uri": "https://anaconda.org/anaconda/streamlit-extras", "description": "A library to discover, try, install and share Streamlit extras" }, + { "name": "streamlit-faker", "uri": "https://anaconda.org/anaconda/streamlit-faker", "description": "streamlit-faker is a library to very easily fake Streamlit commands" }, + { "name": "streamlit-image-coordinates", "uri": "https://anaconda.org/anaconda/streamlit-image-coordinates", "description": "Streamlit component that displays an image and returns the coordinates when you click on it" }, + { "name": "streamlit-keyup", "uri": "https://anaconda.org/anaconda/streamlit-keyup", "description": "Text input that renders on keyup" }, + { "name": "streamlit-toggle-switch", "uri": "https://anaconda.org/anaconda/streamlit-toggle-switch", "description": "Creates a customizable toggle" }, + { "name": "streamlit-vertical-slider", "uri": "https://anaconda.org/anaconda/streamlit-vertical-slider", "description": "Creates a customizable vertical slider" }, + { "name": "streamz", "uri": "https://anaconda.org/anaconda/streamz", "description": "Manage streaming data, optionally with Dask and Pandas" }, + { "name": "strict-rfc3339", "uri": "https://anaconda.org/anaconda/strict-rfc3339", "description": "Strict, simple, lightweight RFC3339 functions" }, + { "name": "stumpy", "uri": "https://anaconda.org/anaconda/stumpy", "description": "A powerful and scalable library that can be used for a variety of time series data mining tasks." }, + { "name": "subprocess32", "uri": "https://anaconda.org/anaconda/subprocess32", "description": "A backport of the subprocess module from Python 3.2/3.3 for use on 2.x" }, + { "name": "sudachidict-core", "uri": "https://anaconda.org/anaconda/sudachidict-core", "description": "Sudachi Dictionary for SudachiPy - Core Edition" }, + { "name": "sudachipy", "uri": "https://anaconda.org/anaconda/sudachipy", "description": "Python version of Sudachi, the Japanese Morphological Analyzer" }, + { "name": "suitesparse", "uri": "https://anaconda.org/anaconda/suitesparse", "description": "A suite of sparse matrix algorithms" }, + { "name": "superqt", "uri": "https://anaconda.org/anaconda/superqt", "description": "Missing widgets and components for PyQt/PySide" }, + { "name": "supervisor", "uri": "https://anaconda.org/anaconda/supervisor", "description": "A Process Control System" }, + { "name": "swagger-ui-bundle", "uri": "https://anaconda.org/anaconda/swagger-ui-bundle", "description": "swagger_ui_bundle - swagger-ui files in a pip package" }, + { "name": "sybil", "uri": "https://anaconda.org/anaconda/sybil", "description": "Automated testing for the examples in your documentation." }, + { "name": "sympy", "uri": "https://anaconda.org/anaconda/sympy", "description": "Python library for symbolic mathematics" }, + { "name": "sysroot-cos7-aarch64", "uri": "https://anaconda.org/anaconda/sysroot-cos7-aarch64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/sysroot-cos7-ppc64le", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot-cos7-s390x", "uri": "https://anaconda.org/anaconda/sysroot-cos7-s390x", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot-cos7-x86_64", "uri": "https://anaconda.org/anaconda/sysroot-cos7-x86_64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot_linux-64", "uri": "https://anaconda.org/anaconda/sysroot_linux-64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot_linux-aarch64", "uri": "https://anaconda.org/anaconda/sysroot_linux-aarch64", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot_linux-ppc64le", "uri": "https://anaconda.org/anaconda/sysroot_linux-ppc64le", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "sysroot_linux-s390x", "uri": "https://anaconda.org/anaconda/sysroot_linux-s390x", "description": "(CDT) The GNU libc libraries and header files for the Linux kernel for use by glibc" }, + { "name": "system-release-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/system-release-amzn2-aarch64", "description": "(CDT) Amazon Linux release files" }, + { "name": "systemd-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/systemd-amzn2-aarch64", "description": "(CDT) A System and Service Manager" }, + { "name": "systemd-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/systemd-cos7-ppc64le", "description": "(CDT) A System and Service Manager" }, + { "name": "systemd-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/systemd-libs-amzn2-aarch64", "description": "(CDT) systemd libraries" }, + { "name": "systemd-libs-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/systemd-libs-cos7-ppc64le", "description": "(CDT) systemd libraries" }, + { "name": "tables", "uri": "https://anaconda.org/anaconda/tables", "description": "Brings together Python, HDF5 and NumPy to easily handle large amounts of data." }, + { "name": "tabpy-server", "uri": "https://anaconda.org/anaconda/tabpy-server", "description": "No Summary" }, + { "name": "tabula-py", "uri": "https://anaconda.org/anaconda/tabula-py", "description": "Simple wrapper of tabula-java: extract table from PDF into pandas DataFrame" }, + { "name": "tabulate", "uri": "https://anaconda.org/anaconda/tabulate", "description": "Pretty-print tabular data in Python, a library and a command-line utility." }, + { "name": "tangled-up-in-unicode", "uri": "https://anaconda.org/anaconda/tangled-up-in-unicode", "description": "Access to the Unicode Character Database (UCD)" }, + { "name": "tapi", "uri": "https://anaconda.org/anaconda/tapi", "description": "TAPI is a Text-based Application Programming Interface" }, + { "name": "tbats", "uri": "https://anaconda.org/anaconda/tbats", "description": "BATS and TBATS for time series forecasting" }, + { "name": "tbb", "uri": "https://anaconda.org/anaconda/tbb", "description": "High level abstract threading library" }, + { "name": "tbb-devel", "uri": "https://anaconda.org/anaconda/tbb-devel", "description": "High level abstract threading library" }, + { "name": "tbb4py", "uri": "https://anaconda.org/anaconda/tbb4py", "description": "TBB module for Python" }, + { "name": "tempora", "uri": "https://anaconda.org/anaconda/tempora", "description": "Objects and routines pertaining to date and time (tempora)" }, + { "name": "tenacity", "uri": "https://anaconda.org/anaconda/tenacity", "description": "Retry a flaky function whenever an exception occurs until it works" }, + { "name": "tensorboard", "uri": "https://anaconda.org/anaconda/tensorboard", "description": "TensorFlow's Visualization Toolkit" }, + { "name": "tensorboard-data-server", "uri": "https://anaconda.org/anaconda/tensorboard-data-server", "description": "Data server for TensorBoard" }, + { "name": "tensorboardx", "uri": "https://anaconda.org/anaconda/tensorboardx", "description": "tensorboard for pytorch" }, + { "name": "tensorflow", "uri": "https://anaconda.org/anaconda/tensorflow", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "tensorflow-base", "uri": "https://anaconda.org/anaconda/tensorflow-base", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "tensorflow-cpu", "uri": "https://anaconda.org/anaconda/tensorflow-cpu", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "tensorflow-datasets", "uri": "https://anaconda.org/anaconda/tensorflow-datasets", "description": "tensorflow/datasets is a library of datasets ready to use with TensorFlow." }, + { "name": "tensorflow-eigen", "uri": "https://anaconda.org/anaconda/tensorflow-eigen", "description": "Metapackage for selecting a TensorFlow variant." }, + { "name": "tensorflow-estimator", "uri": "https://anaconda.org/anaconda/tensorflow-estimator", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "tensorflow-gpu", "uri": "https://anaconda.org/anaconda/tensorflow-gpu", "description": "TensorFlow is an end-to-end open source platform for machine learning." }, + { "name": "tensorflow-gpu-base", "uri": "https://anaconda.org/anaconda/tensorflow-gpu-base", "description": "TensorFlow is a machine learning library, base GPU package, tensorflow only." }, + { "name": "tensorflow-hub", "uri": "https://anaconda.org/anaconda/tensorflow-hub", "description": "A library for transfer learning by reusing parts of TensorFlow models." }, + { "name": "tensorflow-metadata", "uri": "https://anaconda.org/anaconda/tensorflow-metadata", "description": "Library and standards for schema and statistics." }, + { "name": "tensorflow-mkl", "uri": "https://anaconda.org/anaconda/tensorflow-mkl", "description": "Metapackage for selecting a TensorFlow variant." }, + { "name": "tensorflow-probability", "uri": "https://anaconda.org/anaconda/tensorflow-probability", "description": "TensorFlow Probability is a library for probabilistic reasoning and statistical analysis in TensorFlow" }, + { "name": "termcolor", "uri": "https://anaconda.org/anaconda/termcolor", "description": "ANSII Color formatting for output in terminal." }, + { "name": "termcolor-cpp", "uri": "https://anaconda.org/anaconda/termcolor-cpp", "description": "Header-only C++ library for printing colored messages to the terminal." }, + { "name": "terminado", "uri": "https://anaconda.org/anaconda/terminado", "description": "Terminals served by tornado websockets" }, + { "name": "tesseract", "uri": "https://anaconda.org/anaconda/tesseract", "description": "An optical character recognition (OCR) engine" }, + { "name": "testfixtures", "uri": "https://anaconda.org/anaconda/testfixtures", "description": "A collection of helpers and mock objects for unit tests and doc tests." }, + { "name": "testpath", "uri": "https://anaconda.org/anaconda/testpath", "description": "Testpath is a collection of utilities for Python code working with files and commands." }, + { "name": "testscenarios", "uri": "https://anaconda.org/anaconda/testscenarios", "description": "Summary of the package" }, + { "name": "testtools", "uri": "https://anaconda.org/anaconda/testtools", "description": "Extensions to the Python standard library unit testing framework" }, + { "name": "texinfo", "uri": "https://anaconda.org/anaconda/texinfo", "description": "The GNU Documentation System." }, + { "name": "texlive-core", "uri": "https://anaconda.org/anaconda/texlive-core", "description": "An easy way to get up and running with the TeX document production system." }, + { "name": "textblob", "uri": "https://anaconda.org/anaconda/textblob", "description": "Simple, Pythonic text processing. Sentiment analysis, part-of-speech tagging, noun phrase parsing, and more." }, + { "name": "textdistance", "uri": "https://anaconda.org/anaconda/textdistance", "description": "TextDistance – python library for comparing distance between two or more sequences by many algorithms." }, + { "name": "texttable", "uri": "https://anaconda.org/anaconda/texttable", "description": "Python module for creating simple ASCII tables" }, + { "name": "the_silver_searcher", "uri": "https://anaconda.org/anaconda/the_silver_searcher", "description": "A code searching tool similar to ack, with a focus on speed." }, + { "name": "theano", "uri": "https://anaconda.org/anaconda/theano", "description": "Optimizing compiler for evaluating mathematical expressions on CPUs and GPUs." }, + { "name": "theano-pymc", "uri": "https://anaconda.org/anaconda/theano-pymc", "description": "An optimizing compiler for evaluating mathematical expressions. Theano-PyMC is a fork of the Theano library maintained by the PyMC developers." }, + { "name": "thefuzz", "uri": "https://anaconda.org/anaconda/thefuzz", "description": "Fuzzy string matching library." }, + { "name": "thinc", "uri": "https://anaconda.org/anaconda/thinc", "description": "A refreshing functional take on deep learning, compatible with your favorite libraries." }, + { "name": "threadloop", "uri": "https://anaconda.org/anaconda/threadloop", "description": "Tornado IOLoop Backed Concurrent Futures" }, + { "name": "threadpoolctl", "uri": "https://anaconda.org/anaconda/threadpoolctl", "description": "Python helpers to control the threadpools of native libraries" }, + { "name": "three-merge", "uri": "https://anaconda.org/anaconda/three-merge", "description": "Simple Python library to perform a 3-way merge between strings" }, + { "name": "thrift", "uri": "https://anaconda.org/anaconda/thrift", "description": "Python bindings for the Apache Thrift RPC system" }, + { "name": "thrift-compiler", "uri": "https://anaconda.org/anaconda/thrift-compiler", "description": "Compiler and C++ libraries and headers for the Apache Thrift RPC system" }, + { "name": "thrift-cpp", "uri": "https://anaconda.org/anaconda/thrift-cpp", "description": "Compiler and C++ libraries and headers for the Apache Thrift RPC system" }, + { "name": "thrift_sasl", "uri": "https://anaconda.org/anaconda/thrift_sasl", "description": "Thrift SASL module that implements TSaslClientTransport" }, + { "name": "thriftpy2", "uri": "https://anaconda.org/anaconda/thriftpy2", "description": "Pure python implementation of Apache Thrift." }, + { "name": "tifffile", "uri": "https://anaconda.org/anaconda/tifffile", "description": "Read and write image data from and to TIFF files." }, + { "name": "tiledb", "uri": "https://anaconda.org/anaconda/tiledb", "description": "TileDB sparse and dense multi-dimensional array data management" }, + { "name": "time-machine", "uri": "https://anaconda.org/anaconda/time-machine", "description": "Travel through time in your tests." }, + { "name": "tini", "uri": "https://anaconda.org/anaconda/tini", "description": "A tiny but valid `init` for containers" }, + { "name": "tinycss", "uri": "https://anaconda.org/anaconda/tinycss", "description": "Tinycss is a complete yet simple CSS parser for Python." }, + { "name": "tinycss2", "uri": "https://anaconda.org/anaconda/tinycss2", "description": "Low-level CSS parser for Python" }, + { "name": "tk", "uri": "https://anaconda.org/anaconda/tk", "description": "A dynamic programming language with GUI support. Bundles Tcl and Tk." }, + { "name": "tktable", "uri": "https://anaconda.org/anaconda/tktable", "description": "Tktable is a 2D editable table widget" }, + { "name": "tldextract", "uri": "https://anaconda.org/anaconda/tldextract", "description": "Accurately separate the TLD from the registered domain andsubdomains of a URL, using the Public Suffix List." }, + { "name": "tmux", "uri": "https://anaconda.org/anaconda/tmux", "description": "A terminal multiplexer." }, + { "name": "tobler", "uri": "https://anaconda.org/anaconda/tobler", "description": "Tobler is a python package for areal interpolation, dasymetric mapping, and change of support." }, + { "name": "tokenizers", "uri": "https://anaconda.org/anaconda/tokenizers", "description": "Fast State-of-the-Art Tokenizers optimized for Research and Production" }, + { "name": "toml", "uri": "https://anaconda.org/anaconda/toml", "description": "Python lib for TOML." }, + { "name": "tomli", "uri": "https://anaconda.org/anaconda/tomli", "description": "A simple TOML parser" }, + { "name": "tomli-w", "uri": "https://anaconda.org/anaconda/tomli-w", "description": "A lil' TOML writer" }, + { "name": "tomlkit", "uri": "https://anaconda.org/anaconda/tomlkit", "description": "Style preserving TOML library" }, + { "name": "toolchain", "uri": "https://anaconda.org/anaconda/toolchain", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_c_linux-64", "uri": "https://anaconda.org/anaconda/toolchain_c_linux-64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_c_linux-aarch64", "uri": "https://anaconda.org/anaconda/toolchain_c_linux-aarch64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_c_linux-ppc64le", "uri": "https://anaconda.org/anaconda/toolchain_c_linux-ppc64le", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_cxx_linux-64", "uri": "https://anaconda.org/anaconda/toolchain_cxx_linux-64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_cxx_linux-aarch64", "uri": "https://anaconda.org/anaconda/toolchain_cxx_linux-aarch64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_cxx_linux-ppc64le", "uri": "https://anaconda.org/anaconda/toolchain_cxx_linux-ppc64le", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_fort_linux-64", "uri": "https://anaconda.org/anaconda/toolchain_fort_linux-64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_fort_linux-aarch64", "uri": "https://anaconda.org/anaconda/toolchain_fort_linux-aarch64", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolchain_fort_linux-ppc64le", "uri": "https://anaconda.org/anaconda/toolchain_fort_linux-ppc64le", "description": "A meta-package to enable the right toolchain." }, + { "name": "toolz", "uri": "https://anaconda.org/anaconda/toolz", "description": "List processing tools and functional utilities" }, + { "name": "torch-lr-finder", "uri": "https://anaconda.org/anaconda/torch-lr-finder", "description": "Pytorch implementation of the learning rate range test" }, + { "name": "torchmetrics", "uri": "https://anaconda.org/anaconda/torchmetrics", "description": "Collection of PyTorch native metrics for easy evaluating machine learning models" }, + { "name": "torchtriton", "uri": "https://anaconda.org/anaconda/torchtriton", "description": "Development repository for the Triton language and compiler" }, + { "name": "torchvision", "uri": "https://anaconda.org/anaconda/torchvision", "description": "Image and video datasets and models for torch deep learning" }, + { "name": "tornado", "uri": "https://anaconda.org/anaconda/tornado", "description": "A Python web framework and asynchronous networking library, originally developed at FriendFeed." }, + { "name": "tornado-json", "uri": "https://anaconda.org/anaconda/tornado-json", "description": "A simple JSON API framework based on Tornado" }, + { "name": "tqdm", "uri": "https://anaconda.org/anaconda/tqdm", "description": "A Fast, Extensible Progress Meter" }, + { "name": "trace-updater", "uri": "https://anaconda.org/anaconda/trace-updater", "description": "Dash component which allows updating a dcc.Graph its traces." }, + { "name": "traceback2", "uri": "https://anaconda.org/anaconda/traceback2", "description": "Backports of the traceback module" }, + { "name": "trafaret", "uri": "https://anaconda.org/anaconda/trafaret", "description": "Ultimate transformation library that supports validation, contexts and aiohttp" }, + { "name": "traitlets", "uri": "https://anaconda.org/anaconda/traitlets", "description": "Configuration system for Python applications" }, + { "name": "traits", "uri": "https://anaconda.org/anaconda/traits", "description": "traits - explicitly typed attributes for Python" }, + { "name": "traittypes", "uri": "https://anaconda.org/anaconda/traittypes", "description": "Trait types for NumPy, SciPy and friends" }, + { "name": "tranquilizer", "uri": "https://anaconda.org/anaconda/tranquilizer", "description": "Put your Python functions to REST" }, + { "name": "transaction", "uri": "https://anaconda.org/anaconda/transaction", "description": "Transaction management for Python" }, + { "name": "transformers", "uri": "https://anaconda.org/anaconda/transformers", "description": "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow" }, + { "name": "treeinterpreter", "uri": "https://anaconda.org/anaconda/treeinterpreter", "description": "Package for interpreting scikit-learn's decision tree and random forest predictions." }, + { "name": "treelib", "uri": "https://anaconda.org/anaconda/treelib", "description": "A Python 2/3 implementation of tree structure." }, + { "name": "triad", "uri": "https://anaconda.org/anaconda/triad", "description": "A collection of python utility functions for Fugue projects" }, + { "name": "trio", "uri": "https://anaconda.org/anaconda/trio", "description": "Trio – a friendly Python library for async concurrency and I/O" }, + { "name": "trio-websocket", "uri": "https://anaconda.org/anaconda/trio-websocket", "description": "WebSocket library for Trio" }, + { "name": "trousers-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/trousers-amzn2-aarch64", "description": "(CDT) TCG's Software Stack v1.2" }, + { "name": "trousers-cos7-s390x", "uri": "https://anaconda.org/anaconda/trousers-cos7-s390x", "description": "(CDT) TCG's Software Stack v1.2" }, + { "name": "trove-classifiers", "uri": "https://anaconda.org/anaconda/trove-classifiers", "description": "Canonical source for classifiers on PyPI (pypi.org)." }, + { "name": "trustme", "uri": "https://anaconda.org/anaconda/trustme", "description": "#1 quality TLS certs while you wait, for the discerning tester" }, + { "name": "truststore", "uri": "https://anaconda.org/anaconda/truststore", "description": "Verify certificates using native system trust stores" }, + { "name": "tsfresh", "uri": "https://anaconda.org/anaconda/tsfresh", "description": "Automatic extraction of relevant features from time series" }, + { "name": "twine", "uri": "https://anaconda.org/anaconda/twine", "description": "Collection of utilities for interacting with PyPI" }, + { "name": "twisted", "uri": "https://anaconda.org/anaconda/twisted", "description": "An event-based framework for internet applications, written in Python" }, + { "name": "twisted-iocpsupport", "uri": "https://anaconda.org/anaconda/twisted-iocpsupport", "description": "An extension for use in the twisted I/O Completion Ports reactor." }, + { "name": "twofish", "uri": "https://anaconda.org/anaconda/twofish", "description": "Bindings for the Twofish implementation by Niels Ferguson" }, + { "name": "twython", "uri": "https://anaconda.org/anaconda/twython", "description": "Actively maintained, pure Python wrapper for the Twitter API. Supports both normal and streaming Twitter APIs" }, + { "name": "typed-ast", "uri": "https://anaconda.org/anaconda/typed-ast", "description": "a fork of Python 2 and 3 ast modules with type comment support" }, + { "name": "typeguard", "uri": "https://anaconda.org/anaconda/typeguard", "description": "Runtime type checker for Python" }, + { "name": "typer", "uri": "https://anaconda.org/anaconda/typer", "description": "A library for building CLI applications" }, + { "name": "types-futures", "uri": "https://anaconda.org/anaconda/types-futures", "description": "Typing stubs for futures" }, + { "name": "types-jsonschema", "uri": "https://anaconda.org/anaconda/types-jsonschema", "description": "Typing stubs for jsonschema" }, + { "name": "types-protobuf", "uri": "https://anaconda.org/anaconda/types-protobuf", "description": "Typing stubs for protobuf" }, + { "name": "types-psutil", "uri": "https://anaconda.org/anaconda/types-psutil", "description": "Typing stubs for psutil" }, + { "name": "types-pytz", "uri": "https://anaconda.org/anaconda/types-pytz", "description": "Typing stubs for pytz" }, + { "name": "types-pyyaml", "uri": "https://anaconda.org/anaconda/types-pyyaml", "description": "Typing stubs for PyYAML" }, + { "name": "types-requests", "uri": "https://anaconda.org/anaconda/types-requests", "description": "Typing stubs for requests" }, + { "name": "types-setuptools", "uri": "https://anaconda.org/anaconda/types-setuptools", "description": "Typing stubs for setuptools" }, + { "name": "types-toml", "uri": "https://anaconda.org/anaconda/types-toml", "description": "Typing stubs for toml" }, + { "name": "types-urllib3", "uri": "https://anaconda.org/anaconda/types-urllib3", "description": "Typing stubs for urllib3" }, + { "name": "typing", "uri": "https://anaconda.org/anaconda/typing", "description": "Type Hints for Python - backport for Python<3.5" }, + { "name": "typing-extensions", "uri": "https://anaconda.org/anaconda/typing-extensions", "description": "Backported and Experimental Type Hints for Python" }, + { "name": "typing_extensions", "uri": "https://anaconda.org/anaconda/typing_extensions", "description": "Backported and Experimental Type Hints for Python" }, + { "name": "typing_inspect", "uri": "https://anaconda.org/anaconda/typing_inspect", "description": "Runtime inspection utilities for Python typing module" }, + { "name": "typogrify", "uri": "https://anaconda.org/anaconda/typogrify", "description": "Filters to enhance web typography, including support for Django & Jinja templates" }, + { "name": "tzdata", "uri": "https://anaconda.org/anaconda/tzdata", "description": "The Time Zone Database (called tz, tzdb or zoneinfo)" }, + { "name": "tzdata-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/tzdata-amzn2-aarch64", "description": "(CDT) Timezone data" }, + { "name": "tzdata-java-cos7-s390x", "uri": "https://anaconda.org/anaconda/tzdata-java-cos7-s390x", "description": "(CDT) Timezone data for Java" }, + { "name": "tzlocal", "uri": "https://anaconda.org/anaconda/tzlocal", "description": "tzinfo object for the local timezone" }, + { "name": "uc-micro-py", "uri": "https://anaconda.org/anaconda/uc-micro-py", "description": "Micro subset of unicode data files for linkify-it-py projects." }, + { "name": "ucrt", "uri": "https://anaconda.org/anaconda/ucrt", "description": "Redistributable files for Windows SDK. This is only needed Windows <10" }, + { "name": "ucrt64-binutils", "uri": "https://anaconda.org/anaconda/ucrt64-binutils", "description": "A set of programs to assemble and manipulate binary and object files (mingw-w64) (repack of MINGW-packages binutils for UCRT64)" }, + { "name": "ucrt64-bzip2", "uri": "https://anaconda.org/anaconda/ucrt64-bzip2", "description": "A high-quality data compression program (mingw-w64) (repack of MINGW-packages bzip2 for UCRT64)" }, + { "name": "ucrt64-crt-git", "uri": "https://anaconda.org/anaconda/ucrt64-crt-git", "description": "MinGW-w64 CRT for Windows (mingw-w64) (repack of MINGW-packages crt-git for UCRT64)" }, + { "name": "ucrt64-expat", "uri": "https://anaconda.org/anaconda/ucrt64-expat", "description": "An XML parser library (mingw-w64) (repack of MINGW-packages expat for UCRT64)" }, + { "name": "ucrt64-gcc", "uri": "https://anaconda.org/anaconda/ucrt64-gcc", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc for UCRT64)" }, + { "name": "ucrt64-gcc-ada", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-ada", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-ada for UCRT64)" }, + { "name": "ucrt64-gcc-fortran", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-fortran", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-fortran for UCRT64)" }, + { "name": "ucrt64-gcc-libgfortran", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-libgfortran", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-libgfortran for UCRT64)" }, + { "name": "ucrt64-gcc-libs", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-libs", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-libs for UCRT64)" }, + { "name": "ucrt64-gcc-lto-dump", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-lto-dump", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-lto-dump for UCRT64)" }, + { "name": "ucrt64-gcc-objc", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-objc", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-objc for UCRT64)" }, + { "name": "ucrt64-gcc-rust", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-rust", "description": "GCC for the MinGW-w64 (repack of MINGW-packages gcc-rust for UCRT64)" }, + { "name": "ucrt64-gcc-toolchain", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-toolchain", "description": "UCRT64 GCC toolchain packages" }, + { "name": "ucrt64-gcc-toolchain_win-64", "uri": "https://anaconda.org/anaconda/ucrt64-gcc-toolchain_win-64", "description": "UCRT64 GCC toolchain metapackage" }, + { "name": "ucrt64-gettext-runtime", "uri": "https://anaconda.org/anaconda/ucrt64-gettext-runtime", "description": "GNU internationalization library (mingw-w64) (repack of MINGW-packages gettext-runtime for UCRT64)" }, + { "name": "ucrt64-gettext-tools", "uri": "https://anaconda.org/anaconda/ucrt64-gettext-tools", "description": "GNU internationalization library (mingw-w64) (repack of MINGW-packages gettext-tools for UCRT64)" }, + { "name": "ucrt64-gmp", "uri": "https://anaconda.org/anaconda/ucrt64-gmp", "description": "A free library for arbitrary precision arithmetic (mingw-w64) (repack of MINGW-packages gmp for UCRT64)" }, + { "name": "ucrt64-headers-git", "uri": "https://anaconda.org/anaconda/ucrt64-headers-git", "description": "MinGW-w64 headers for Windows (mingw-w64) (repack of MINGW-packages headers-git for UCRT64)" }, + { "name": "ucrt64-iconv", "uri": "https://anaconda.org/anaconda/ucrt64-iconv", "description": "Character encoding conversion library and utility (mingw-w64) (repack of MINGW-packages iconv for UCRT64)" }, + { "name": "ucrt64-isl", "uri": "https://anaconda.org/anaconda/ucrt64-isl", "description": "Library for manipulating sets and relations of integer points bounded by linear constraints (mingw-w64) (repack of MINGW-packages isl for UCRT64)" }, + { "name": "ucrt64-libcxx", "uri": "https://anaconda.org/anaconda/ucrt64-libcxx", "description": "(repack of MINGW-packages libc++ for UCRT64)" }, + { "name": "ucrt64-libgccjit", "uri": "https://anaconda.org/anaconda/ucrt64-libgccjit", "description": "GCC for the MinGW-w64 (repack of MINGW-packages libgccjit for UCRT64)" }, + { "name": "ucrt64-libiconv", "uri": "https://anaconda.org/anaconda/ucrt64-libiconv", "description": "Character encoding conversion library and utility (mingw-w64) (repack of MINGW-packages libiconv for UCRT64)" }, + { "name": "ucrt64-libidn2", "uri": "https://anaconda.org/anaconda/ucrt64-libidn2", "description": "Implementation of the Stringprep, Punycode and IDNA specifications (mingw-w64) (repack of MINGW-packages libidn2 for UCRT64)" }, + { "name": "ucrt64-libmangle-git", "uri": "https://anaconda.org/anaconda/ucrt64-libmangle-git", "description": "MinGW-w64 libmangle (mingw-w64) (repack of MINGW-packages libmangle-git for UCRT64)" }, + { "name": "ucrt64-libunistring", "uri": "https://anaconda.org/anaconda/ucrt64-libunistring", "description": "Library for manipulating Unicode strings and C strings. (mingw-w64) (repack of MINGW-packages libunistring for UCRT64)" }, + { "name": "ucrt64-libunwind", "uri": "https://anaconda.org/anaconda/ucrt64-libunwind", "description": "(repack of MINGW-packages libunwind for UCRT64)" }, + { "name": "ucrt64-libwinpthread-git", "uri": "https://anaconda.org/anaconda/ucrt64-libwinpthread-git", "description": "MinGW-w64 winpthreads library (mingw-w64) (repack of MINGW-packages libwinpthread-git for UCRT64)" }, + { "name": "ucrt64-make", "uri": "https://anaconda.org/anaconda/ucrt64-make", "description": "GNU make utility to maintain groups of programs (mingw-w64) (repack of MINGW-packages make for UCRT64)" }, + { "name": "ucrt64-minizip", "uri": "https://anaconda.org/anaconda/ucrt64-minizip", "description": "(repack of MINGW-packages minizip for UCRT64)" }, + { "name": "ucrt64-mpc", "uri": "https://anaconda.org/anaconda/ucrt64-mpc", "description": "Multiple precision complex arithmetic library (mingw-w64) (repack of MINGW-packages mpc for UCRT64)" }, + { "name": "ucrt64-mpfr", "uri": "https://anaconda.org/anaconda/ucrt64-mpfr", "description": "Multiple-precision floating-point library (mingw-w64) (repack of MINGW-packages mpfr for UCRT64)" }, + { "name": "ucrt64-openblas", "uri": "https://anaconda.org/anaconda/ucrt64-openblas", "description": "An optimized BLAS library based on GotoBLAS2 1.13 BSD, providing optimized blas, lapack, and cblas (mingw-w64) (repack of MINGW-packages openblas for UCRT64)" }, + { "name": "ucrt64-openblas64", "uri": "https://anaconda.org/anaconda/ucrt64-openblas64", "description": "An optimized BLAS library based on GotoBLAS2 1.13 BSD, providing optimized blas, lapack, and cblas (mingw-w64) (repack of MINGW-packages openblas64 for UCRT64)" }, + { "name": "ucrt64-pkg-config", "uri": "https://anaconda.org/anaconda/ucrt64-pkg-config", "description": "A system for managing library compile/link flags (mingw-w64) (repack of MINGW-packages pkg-config for UCRT64)" }, + { "name": "ucrt64-tools-git", "uri": "https://anaconda.org/anaconda/ucrt64-tools-git", "description": "MinGW-w64 tools (mingw-w64) (repack of MINGW-packages tools-git for UCRT64)" }, + { "name": "ucrt64-windows-default-manifest", "uri": "https://anaconda.org/anaconda/ucrt64-windows-default-manifest", "description": "Default Windows application manifest (mingw-w64) (repack of MINGW-packages windows-default-manifest for UCRT64)" }, + { "name": "ucrt64-winpthreads-git", "uri": "https://anaconda.org/anaconda/ucrt64-winpthreads-git", "description": "MinGW-w64 winpthreads library (mingw-w64) (repack of MINGW-packages winpthreads-git for UCRT64)" }, + { "name": "ucrt64-xz", "uri": "https://anaconda.org/anaconda/ucrt64-xz", "description": "Library and command line tools for XZ and LZMA compressed files (mingw-w64) (repack of MINGW-packages xz for UCRT64)" }, + { "name": "ucrt64-zlib", "uri": "https://anaconda.org/anaconda/ucrt64-zlib", "description": "(repack of MINGW-packages zlib for UCRT64)" }, + { "name": "ucrt64-zstd", "uri": "https://anaconda.org/anaconda/ucrt64-zstd", "description": "Zstandard - Fast real-time compression algorithm (mingw-w64) (repack of MINGW-packages zstd for UCRT64)" }, + { "name": "udev-cos6-x86_64", "uri": "https://anaconda.org/anaconda/udev-cos6-x86_64", "description": "(CDT) A userspace implementation of devfs" }, + { "name": "ujson", "uri": "https://anaconda.org/anaconda/ujson", "description": "Ultra fast JSON decoder and encoder written in C with Python bindings" }, + { "name": "ukkonen", "uri": "https://anaconda.org/anaconda/ukkonen", "description": "Implementation of bounded Levenshtein distance (Ukkonen)." }, + { "name": "umap-learn", "uri": "https://anaconda.org/anaconda/umap-learn", "description": "Uniform Manifold Approximation and Projection" }, + { "name": "unearth", "uri": "https://anaconda.org/anaconda/unearth", "description": "A utility to fetch and download python packages" }, + { "name": "unicodedata2", "uri": "https://anaconda.org/anaconda/unicodedata2", "description": "unicodedata backport/updates to python 3 and python 2." }, + { "name": "unidecode", "uri": "https://anaconda.org/anaconda/unidecode", "description": "ASCII transliterations of Unicode text" }, + { "name": "unittest-xml-reporting", "uri": "https://anaconda.org/anaconda/unittest-xml-reporting", "description": "unittest-based test runner with Ant/JUnit like XML reporting." }, + { "name": "unqlite", "uri": "https://anaconda.org/anaconda/unqlite", "description": "Fast Python bindings for the UnQLite embedded NoSQL database." }, + { "name": "unyt", "uri": "https://anaconda.org/anaconda/unyt", "description": "Handle, manipulate, and convert data with units in Python" }, + { "name": "unzip", "uri": "https://anaconda.org/anaconda/unzip", "description": "simple program for unzipping files" }, + { "name": "uriparser", "uri": "https://anaconda.org/anaconda/uriparser", "description": "RFC 3986 compliant URI parsing and handling library written in C89" }, + { "name": "url-normalize", "uri": "https://anaconda.org/anaconda/url-normalize", "description": "URL normalization for Python" }, + { "name": "urllib3", "uri": "https://anaconda.org/anaconda/urllib3", "description": "HTTP library with thread-safe connection pooling, file post, and more." }, + { "name": "urwid", "uri": "https://anaconda.org/anaconda/urwid", "description": "A full-featured console (xterm et al.) user interface library" }, + { "name": "ustr-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/ustr-amzn2-aarch64", "description": "(CDT) String library, very low memory overhead, simple to import" }, + { "name": "utf8proc", "uri": "https://anaconda.org/anaconda/utf8proc", "description": "utf8proc is a small C library that provides Unicode utility functions" }, + { "name": "utfcpp", "uri": "https://anaconda.org/anaconda/utfcpp", "description": "A simple, portable and lightweight generic library for handling UTF-8 encoded strings." }, + { "name": "util-linux-ng-cos6-x86_64", "uri": "https://anaconda.org/anaconda/util-linux-ng-cos6-x86_64", "description": "(CDT) A collection of basic system utilities" }, + { "name": "uvicorn", "uri": "https://anaconda.org/anaconda/uvicorn", "description": "The lightning-fast ASGI server." }, + { "name": "uvicorn-standard", "uri": "https://anaconda.org/anaconda/uvicorn-standard", "description": "The lightning-fast ASGI server." }, + { "name": "uvloop", "uri": "https://anaconda.org/anaconda/uvloop", "description": "Ultra fast implementation of asyncio event loop on top of libuv." }, + { "name": "uwsgi", "uri": "https://anaconda.org/anaconda/uwsgi", "description": "The uWSGI project aims at developing a full stack for building hosting services" }, + { "name": "validators", "uri": "https://anaconda.org/anaconda/validators", "description": "Python Data Validation for Humans" }, + { "name": "vc", "uri": "https://anaconda.org/anaconda/vc", "description": "A meta-package to impose mutual exclusivity among software built with different VS versions" }, + { "name": "vcrpy", "uri": "https://anaconda.org/anaconda/vcrpy", "description": "Automatically mock your HTTP interactions to simplify and speed up testing" }, + { "name": "vega_datasets", "uri": "https://anaconda.org/anaconda/vega_datasets", "description": "A Python package for offline access to Vega datasets" }, + { "name": "verboselogs", "uri": "https://anaconda.org/anaconda/verboselogs", "description": "Verbose logging level for Python's logging module." }, + { "name": "versioneer", "uri": "https://anaconda.org/anaconda/versioneer", "description": "Easy VCS-based management of project version strings" }, + { "name": "vertica-python", "uri": "https://anaconda.org/anaconda/vertica-python", "description": "A native Python client for the Vertica database." }, + { "name": "vine", "uri": "https://anaconda.org/anaconda/vine", "description": "Python promises" }, + { "name": "virtualenv", "uri": "https://anaconda.org/anaconda/virtualenv", "description": "Virtual Python Environment builder" }, + { "name": "virtualenv-clone", "uri": "https://anaconda.org/anaconda/virtualenv-clone", "description": "script to clone virtualenvs." }, + { "name": "visions", "uri": "https://anaconda.org/anaconda/visions", "description": "Type System for Data Analysis in Python" }, + { "name": "voila", "uri": "https://anaconda.org/anaconda/voila", "description": "Rendering of live Jupyter notebooks with interactive widgets" }, + { "name": "vs2015_runtime", "uri": "https://anaconda.org/anaconda/vs2015_runtime", "description": "MSVC runtimes associated with cl.exe version 19.40.33813 (VS 2022 update 10)" }, + { "name": "vs2017_win-32", "uri": "https://anaconda.org/anaconda/vs2017_win-32", "description": "Activation and version verification of MSVC 14.1 (VS 2017 compiler, update 9)" }, + { "name": "vs2017_win-64", "uri": "https://anaconda.org/anaconda/vs2017_win-64", "description": "Activation and version verification of MSVC 14.1 (VS 2017 compiler, update 9)" }, + { "name": "vs2019_win-32", "uri": "https://anaconda.org/anaconda/vs2019_win-32", "description": "Activation and version verification of MSVC 14.2 (VS 2019 compiler, update 5)" }, + { "name": "vs2019_win-64", "uri": "https://anaconda.org/anaconda/vs2019_win-64", "description": "Activation and version verification of MSVC 14.2 (VS 2019 compiler, update 11)" }, + { "name": "vs2022_win-64", "uri": "https://anaconda.org/anaconda/vs2022_win-64", "description": "Activation and version verification of MSVC 14.40 (VS 2022 compiler, update 10)" }, + { "name": "vswhere", "uri": "https://anaconda.org/anaconda/vswhere", "description": "CLI tool to locate Visual Studio 2017 and newer installations" }, + { "name": "vtk", "uri": "https://anaconda.org/anaconda/vtk", "description": "The Visualization Toolkit (VTK) is an open-source, freely available software system for 3D computer graphics, modeling, image processing, volume rendering, scientific visualization, and information visualization." }, + { "name": "vyper-config", "uri": "https://anaconda.org/anaconda/vyper-config", "description": "Python configuration with (more) fangs" }, + { "name": "w3lib", "uri": "https://anaconda.org/anaconda/w3lib", "description": "Library of web-related functions" }, + { "name": "waf", "uri": "https://anaconda.org/anaconda/waf", "description": "A build automation tool." }, + { "name": "waitress", "uri": "https://anaconda.org/anaconda/waitress", "description": "Production-quality pure-Python WSGI server" }, + { "name": "wasabi", "uri": "https://anaconda.org/anaconda/wasabi", "description": "A lightweight console printing and formatting toolkit" }, + { "name": "wasmtime", "uri": "https://anaconda.org/anaconda/wasmtime", "description": "A WebAssembly runtime powered by Wasmtime" }, + { "name": "watchdog", "uri": "https://anaconda.org/anaconda/watchdog", "description": "Filesystem events monitoring" }, + { "name": "watchfiles", "uri": "https://anaconda.org/anaconda/watchfiles", "description": "Simple, modern and high performance file watching and code reload in python." }, + { "name": "weasel", "uri": "https://anaconda.org/anaconda/weasel", "description": "A small and easy workflow system" }, + { "name": "webencodings", "uri": "https://anaconda.org/anaconda/webencodings", "description": "Character encoding aliases for legacy web content" }, + { "name": "webkitgtk-cos6-i686", "uri": "https://anaconda.org/anaconda/webkitgtk-cos6-i686", "description": "(CDT) GTK+ Web content engine library" }, + { "name": "webkitgtk-cos6-x86_64", "uri": "https://anaconda.org/anaconda/webkitgtk-cos6-x86_64", "description": "(CDT) GTK+ Web content engine library" }, + { "name": "webkitgtk-devel-cos6-i686", "uri": "https://anaconda.org/anaconda/webkitgtk-devel-cos6-i686", "description": "(CDT) Development files for webkitgtk" }, + { "name": "webkitgtk-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/webkitgtk-devel-cos6-x86_64", "description": "(CDT) Development files for webkitgtk" }, + { "name": "webkitgtk3-cos7-s390x", "uri": "https://anaconda.org/anaconda/webkitgtk3-cos7-s390x", "description": "(CDT) GTK+ Web content engine library" }, + { "name": "webkitgtk3-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/webkitgtk3-devel-cos7-s390x", "description": "(CDT) Development files for webkitgtk3" }, + { "name": "websocket-client", "uri": "https://anaconda.org/anaconda/websocket-client", "description": "WebSocket client for Python" }, + { "name": "websockets", "uri": "https://anaconda.org/anaconda/websockets", "description": "A library for developing WebSocket servers and clients in Python." }, + { "name": "webtest", "uri": "https://anaconda.org/anaconda/webtest", "description": "helper to test WSGI applications" }, + { "name": "werkzeug", "uri": "https://anaconda.org/anaconda/werkzeug", "description": "The comprehensive WSGI web application library." }, + { "name": "wget", "uri": "https://anaconda.org/anaconda/wget", "description": "No Summary" }, + { "name": "whatthepatch", "uri": "https://anaconda.org/anaconda/whatthepatch", "description": "What The Patch!? is a library for both parsing and applying patch files" }, + { "name": "wheel", "uri": "https://anaconda.org/anaconda/wheel", "description": "A built-package format for Python." }, + { "name": "whichcraft", "uri": "https://anaconda.org/anaconda/whichcraft", "description": "This package provides cross-platform cross-python shutil.which functionality." }, + { "name": "whylabs-client", "uri": "https://anaconda.org/anaconda/whylabs-client", "description": "Public Python client for WhyLabs API" }, + { "name": "whylogs-sketching", "uri": "https://anaconda.org/anaconda/whylogs-sketching", "description": "WhyLabs's fork of the Apache Datasketches library" }, + { "name": "widgetsnbextension", "uri": "https://anaconda.org/anaconda/widgetsnbextension", "description": "Interactive Widgets for Jupyter" }, + { "name": "win32_setctime", "uri": "https://anaconda.org/anaconda/win32_setctime", "description": "A small Python utility to set file creation time on Windows" }, + { "name": "win_inet_pton", "uri": "https://anaconda.org/anaconda/win_inet_pton", "description": "Native inet_pton and inet_ntop implementation for Python on Windows (with ctypes)." }, + { "name": "winpty", "uri": "https://anaconda.org/anaconda/winpty", "description": "Winpty provides an interface similar to a Unix pty-master for communicating\nwith Windows console programs." }, + { "name": "winreg", "uri": "https://anaconda.org/anaconda/winreg", "description": "Convenient high-level C++ wrapper around the Windows Registry API" }, + { "name": "winsdk", "uri": "https://anaconda.org/anaconda/winsdk", "description": "Scripts to download Windows SDK headers" }, + { "name": "woodwork", "uri": "https://anaconda.org/anaconda/woodwork", "description": "Woodwork is a Python library that provides robust methods for managing and communicating data typing information." }, + { "name": "word2vec", "uri": "https://anaconda.org/anaconda/word2vec", "description": "Python interface to Google word2vec" }, + { "name": "wordcloud", "uri": "https://anaconda.org/anaconda/wordcloud", "description": "A little word cloud generator in Python" }, + { "name": "workerpool", "uri": "https://anaconda.org/anaconda/workerpool", "description": "No Summary" }, + { "name": "wrapt", "uri": "https://anaconda.org/anaconda/wrapt", "description": "Module for decorators, wrappers and monkey patching" }, + { "name": "ws4py", "uri": "https://anaconda.org/anaconda/ws4py", "description": "WebSocket client and server library for Python 2, 3, and PyPy" }, + { "name": "wsgiproxy2", "uri": "https://anaconda.org/anaconda/wsgiproxy2", "description": "A WSGI Proxy with various http client backends" }, + { "name": "wsgiref", "uri": "https://anaconda.org/anaconda/wsgiref", "description": "WSGI (PEP 333) Reference Library" }, + { "name": "wsproto", "uri": "https://anaconda.org/anaconda/wsproto", "description": "Pure Python, pure state-machine WebSocket implementation." }, + { "name": "wstools", "uri": "https://anaconda.org/anaconda/wstools", "description": "WSDL parsing services package for Web Services for Python" }, + { "name": "wurlitzer", "uri": "https://anaconda.org/anaconda/wurlitzer", "description": "Capture C-level stdout/stderr in Python" }, + { "name": "x264", "uri": "https://anaconda.org/anaconda/x264", "description": "A free software library for encoding video streams into the H.264/MPEG-4 AVC format." }, + { "name": "xar", "uri": "https://anaconda.org/anaconda/xar", "description": "eXtensible ARchiver" }, + { "name": "xarray", "uri": "https://anaconda.org/anaconda/xarray", "description": "N-D labeled arrays and datasets in Python." }, + { "name": "xarray-einstats", "uri": "https://anaconda.org/anaconda/xarray-einstats", "description": "Stats, linear algebra and einops for xarray." }, + { "name": "xattr", "uri": "https://anaconda.org/anaconda/xattr", "description": "Python wrapper for extended filesystem attributes" }, + { "name": "xcb-proto", "uri": "https://anaconda.org/anaconda/xcb-proto", "description": "Provides the XML-XCB protocol descriptions that libxcb uses to generate the majority of its code and API" }, + { "name": "xcb-util-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-cursor", "uri": "https://anaconda.org/anaconda/xcb-util-cursor", "description": "port of Xlib libXcursor functions" }, + { "name": "xcb-util-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-devel-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-devel-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-image-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-image-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-image-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-image-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-image-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-image-devel-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-image-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-image-devel-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-keysyms-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-keysyms-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-keysyms-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-keysyms-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-keysyms-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-keysyms-devel-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-keysyms-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-keysyms-devel-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-renderutil-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-renderutil-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-renderutil-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-renderutil-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-renderutil-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-renderutil-devel-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-renderutil-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-renderutil-devel-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-wm-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-wm-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-wm-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-wm-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-wm-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xcb-util-wm-devel-amzn2-aarch64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xcb-util-wm-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xcb-util-wm-devel-cos6-x86_64", "description": "(CDT) A C binding to the X11 protocol" }, + { "name": "xerces-c", "uri": "https://anaconda.org/anaconda/xerces-c", "description": "Xerces-C++ is a validating XML parser written in a portable subset of C++." }, + { "name": "xgboost", "uri": "https://anaconda.org/anaconda/xgboost", "description": "Scalable, Portable and Distributed Gradient Boosting (GBDT, GBRT or GBM) Library, for\nPython, R, Java, Scala, C++ and more. Runs on single machine, Hadoop, Spark, Flink\nand DataFlow" }, + { "name": "xlsxwriter", "uri": "https://anaconda.org/anaconda/xlsxwriter", "description": "A Python module for creating Excel XLSX files" }, + { "name": "xlwings", "uri": "https://anaconda.org/anaconda/xlwings", "description": "Interact with Excel from Python and vice versa" }, + { "name": "xmlsec", "uri": "https://anaconda.org/anaconda/xmlsec", "description": "Python bindings for the XML Security Library (XMLSec)." }, + { "name": "xmltodict", "uri": "https://anaconda.org/anaconda/xmltodict", "description": "Makes working with XML feel like you are working with JSON" }, + { "name": "xorg-util-macros", "uri": "https://anaconda.org/anaconda/xorg-util-macros", "description": "Development utility macros for X.org software." }, + { "name": "xorg-x11-proto-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xorg-x11-proto-devel-amzn2-aarch64", "description": "(CDT) X.Org X11 Protocol headers" }, + { "name": "xorg-x11-proto-devel-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xorg-x11-proto-devel-cos6-x86_64", "description": "(CDT) X.Org X11 Protocol headers" }, + { "name": "xorg-x11-proto-devel-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/xorg-x11-proto-devel-cos7-ppc64le", "description": "(CDT) X.Org X11 Protocol headers" }, + { "name": "xorg-x11-proto-devel-cos7-s390x", "uri": "https://anaconda.org/anaconda/xorg-x11-proto-devel-cos7-s390x", "description": "(CDT) X.Org X11 Protocol headers" }, + { "name": "xorg-x11-server-common-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xorg-x11-server-common-cos6-x86_64", "description": "(CDT) Xorg server common files" }, + { "name": "xorg-x11-server-common-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/xorg-x11-server-common-cos7-ppc64le", "description": "(CDT) Xorg server common files" }, + { "name": "xorg-x11-server-utils-cos7-s390x", "uri": "https://anaconda.org/anaconda/xorg-x11-server-utils-cos7-s390x", "description": "(CDT) X.Org X11 X server utilities" }, + { "name": "xorg-x11-server-xvfb-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xorg-x11-server-xvfb-cos6-x86_64", "description": "(CDT) A X Windows System virtual framebuffer X server." }, + { "name": "xorg-x11-server-xvfb-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/xorg-x11-server-xvfb-cos7-ppc64le", "description": "(CDT) A X Windows System virtual framebuffer X server." }, + { "name": "xorg-x11-util-macros-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xorg-x11-util-macros-amzn2-aarch64", "description": "(CDT) X.Org X11 Autotools macros" }, + { "name": "xorg-x11-util-macros-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xorg-x11-util-macros-cos6-x86_64", "description": "(CDT) X.Org X11 Autotools macros" }, + { "name": "xorg-x11-util-macros-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/xorg-x11-util-macros-cos7-ppc64le", "description": "(CDT) X.Org X11 Autotools macros" }, + { "name": "xorg-x11-util-macros-cos7-s390x", "uri": "https://anaconda.org/anaconda/xorg-x11-util-macros-cos7-s390x", "description": "(CDT) X.Org X11 Autotools macros" }, + { "name": "xorg-x11-xauth-cos6-x86_64", "uri": "https://anaconda.org/anaconda/xorg-x11-xauth-cos6-x86_64", "description": "(CDT) X.Org X11 X authority utilities" }, + { "name": "xorg-x11-xauth-cos7-ppc64le", "uri": "https://anaconda.org/anaconda/xorg-x11-xauth-cos7-ppc64le", "description": "(CDT) X.Org X11 X authority utilities" }, + { "name": "xorg-x11-xauth-cos7-s390x", "uri": "https://anaconda.org/anaconda/xorg-x11-xauth-cos7-s390x", "description": "(CDT) X.Org X11 X authority utilities" }, + { "name": "xorg-xproto", "uri": "https://anaconda.org/anaconda/xorg-xproto", "description": "Core X Windows C prototypes." }, + { "name": "xsimd", "uri": "https://anaconda.org/anaconda/xsimd", "description": "C++ Wrappers for SIMD Intrinsices" }, + { "name": "xxhash", "uri": "https://anaconda.org/anaconda/xxhash", "description": "Extremely fast hash algorithm" }, + { "name": "xyzservices", "uri": "https://anaconda.org/anaconda/xyzservices", "description": "Source of XYZ tiles providers" }, + { "name": "xz", "uri": "https://anaconda.org/anaconda/xz", "description": "Data compression software with high compression ratio" }, + { "name": "xz-libs-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/xz-libs-amzn2-aarch64", "description": "(CDT) Libraries for decoding LZMA compression" }, + { "name": "xz-static", "uri": "https://anaconda.org/anaconda/xz-static", "description": "Data compression software with high compression ratio" }, + { "name": "y-py", "uri": "https://anaconda.org/anaconda/y-py", "description": "Python bindings for the Rust port of Yjs" }, + { "name": "yacs", "uri": "https://anaconda.org/anaconda/yacs", "description": "YACS -- Yet Another Configuration System" }, + { "name": "yajl", "uri": "https://anaconda.org/anaconda/yajl", "description": "Yet Another JSON Library" }, + { "name": "yaml-cpp", "uri": "https://anaconda.org/anaconda/yaml-cpp", "description": "yaml-cpp is a YAML parser and emitter in C++ matching the YAML 1.2 spec." }, + { "name": "yaml-cpp-static", "uri": "https://anaconda.org/anaconda/yaml-cpp-static", "description": "yaml-cpp is a YAML parser and emitter in C++ matching the YAML 1.2 spec." }, + { "name": "yapf", "uri": "https://anaconda.org/anaconda/yapf", "description": "A formatter for Python code" }, + { "name": "yarl", "uri": "https://anaconda.org/anaconda/yarl", "description": "Yet another URL library" }, + { "name": "yarn", "uri": "https://anaconda.org/anaconda/yarn", "description": "Fast, reliable, and secure dependency management." }, + { "name": "yasm", "uri": "https://anaconda.org/anaconda/yasm", "description": "Yasm is a complete rewrite of the NASM assembler under the \"new\" BSD License." }, + { "name": "ydata-profiling", "uri": "https://anaconda.org/anaconda/ydata-profiling", "description": "Generate profile report for pandas DataFrame" }, + { "name": "yellowbrick", "uri": "https://anaconda.org/anaconda/yellowbrick", "description": "A suite of visual analysis and diagnostic tools for machine learning." }, + { "name": "ypy-websocket", "uri": "https://anaconda.org/anaconda/ypy-websocket", "description": "WebSocket connector for Ypy" }, + { "name": "yq", "uri": "https://anaconda.org/anaconda/yq", "description": "Command-line YAML/XML processor - jq wrapper for YAML/XML documents" }, + { "name": "yt", "uri": "https://anaconda.org/anaconda/yt", "description": "Analysis and visualization toolkit for volumetric data" }, + { "name": "zarr", "uri": "https://anaconda.org/anaconda/zarr", "description": "An implementation of chunked, compressed, N-dimensional arrays for Python." }, + { "name": "zc.lockfile", "uri": "https://anaconda.org/anaconda/zc.lockfile", "description": "Basic inter-process locks" }, + { "name": "zeep", "uri": "https://anaconda.org/anaconda/zeep", "description": "A fast and modern Python SOAP client" }, + { "name": "zeromq", "uri": "https://anaconda.org/anaconda/zeromq", "description": "A high-performance asynchronous messaging library." }, + { "name": "zeromq-static", "uri": "https://anaconda.org/anaconda/zeromq-static", "description": "A high-performance asynchronous messaging library." }, + { "name": "zfp", "uri": "https://anaconda.org/anaconda/zfp", "description": "Library for compressed numerical arrays that support high throughput read and write random access" }, + { "name": "zfpy", "uri": "https://anaconda.org/anaconda/zfpy", "description": "Library for compressed numerical arrays that support high throughput read and write random access" }, + { "name": "zict", "uri": "https://anaconda.org/anaconda/zict", "description": "Composable Dictionary Classes" }, + { "name": "zip", "uri": "https://anaconda.org/anaconda/zip", "description": "simple program for unzipping files" }, + { "name": "zip-cos6-x86_64", "uri": "https://anaconda.org/anaconda/zip-cos6-x86_64", "description": "(CDT) A file compression and packaging utility compatible with PKZIP" }, + { "name": "zip-cos7-s390x", "uri": "https://anaconda.org/anaconda/zip-cos7-s390x", "description": "(CDT) A file compression and packaging utility compatible with PKZIP" }, + { "name": "zipfile-deflate64", "uri": "https://anaconda.org/anaconda/zipfile-deflate64", "description": "Extract Deflate64 ZIP archives with Python's zipfile API." }, + { "name": "zipfile36", "uri": "https://anaconda.org/anaconda/zipfile36", "description": "Backport of zipfile from Python 3.6" }, + { "name": "zipp", "uri": "https://anaconda.org/anaconda/zipp", "description": "A pathlib-compatible Zipfile object wrapper" }, + { "name": "zlib", "uri": "https://anaconda.org/anaconda/zlib", "description": "Massively spiffy yet delicately unobtrusive compression library" }, + { "name": "zlib-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/zlib-amzn2-aarch64", "description": "(CDT) The compression and decompression library" }, + { "name": "zlib-devel-amzn2-aarch64", "uri": "https://anaconda.org/anaconda/zlib-devel-amzn2-aarch64", "description": "(CDT) Header files and libraries for Zlib development" }, + { "name": "zlib-ng", "uri": "https://anaconda.org/anaconda/zlib-ng", "description": "zlib data compression library for the next generation systems" }, + { "name": "zope", "uri": "https://anaconda.org/anaconda/zope", "description": "No Summary" }, + { "name": "zope.component", "uri": "https://anaconda.org/anaconda/zope.component", "description": "Zope Component Architecture" }, + { "name": "zope.event", "uri": "https://anaconda.org/anaconda/zope.event", "description": "Event publishing / dispatch, used by Zope Component Architecture" }, + { "name": "zope.interface", "uri": "https://anaconda.org/anaconda/zope.interface", "description": "Interfaces for Python" }, + { "name": "zstandard", "uri": "https://anaconda.org/anaconda/zstandard", "description": "Zstandard bindings for Python" }, + { "name": "zstd", "uri": "https://anaconda.org/anaconda/zstd", "description": "Zstandard - Fast real-time compression algorithm" }, + { "name": "zstd-static", "uri": "https://anaconda.org/anaconda/zstd-static", "description": "Zstandard - Fast real-time compression algorithm" } +] diff --git a/files/templates/copilot-instructions-text/package-copilot-instructions.md b/files/templates/copilot-instructions-text/package-copilot-instructions.md new file mode 100644 index 0000000..0451ed4 --- /dev/null +++ b/files/templates/copilot-instructions-text/package-copilot-instructions.md @@ -0,0 +1,10 @@ +# Copilot Instructions for + +- The package `` is a Python Project located in the folder `-folder`. +- You need to call the `Get Python Environment Information` tool on the `` path to get the Python executable details. +- Substitute the Python executable you get from the `Get Python Environment Information` tool anywhere you see `` in these instructions. + - Run command for ``: ` -m ` + - Command to run tests for ``: ` -m pytest /tests` +- To run an editable install for the package ``, use the `Install Python Package` tool with the `-folder` path and arguments `['-e', '.']`. +- In the workspace `launch.json` file, configurations related to this package have the prefix ``. +- The package `` has a defined `pyproject.toml` file that you should use and keep up to date. diff --git a/files/templates/copilot-instructions-text/script-copilot-instructions.md b/files/templates/copilot-instructions-text/script-copilot-instructions.md new file mode 100644 index 0000000..01bd826 --- /dev/null +++ b/files/templates/copilot-instructions-text/script-copilot-instructions.md @@ -0,0 +1,9 @@ +# Copilot Instructions for + +- The script `` is a Python python project within the workspace. +- It has inline script metadata (as proposed by PEP 723) that defines the script name, required python version, and dependencies. +- If imports which require a specific Python version or dependencies are added, keep the inline script metadata up to date. +- You need to call the `Get Python Environment Information` tool on the `` path to get the Python executable details. +- Substitute the Python executable you get from the `Get Python Environment Information` tool anywhere you see `` in these instructions. + - Run command for ``: ` ` + - Script can be easily debugged from the Integrated Terminal when activated with the command `debugpy ` after the necessary environment is activated. diff --git a/files/templates/new723ScriptTemplate/script.py b/files/templates/new723ScriptTemplate/script.py new file mode 100644 index 0000000..1c57fc6 --- /dev/null +++ b/files/templates/new723ScriptTemplate/script.py @@ -0,0 +1,16 @@ +# /// script_name +# requires-python = ">=X.XX" TODO: Update this to the minimum Python version you want to support +# dependencies = [ +# TODO: Add any dependencies your script requires +# ] +# /// + +# TODO: Update the main function to your needs or remove it. + + +def main() -> None: + print("Start coding in Python today!") + + +if __name__ == "__main__": + main() diff --git a/files/templates/newPackageTemplate/dev-requirements.txt b/files/templates/newPackageTemplate/dev-requirements.txt new file mode 100644 index 0000000..c16a051 --- /dev/null +++ b/files/templates/newPackageTemplate/dev-requirements.txt @@ -0,0 +1,4 @@ +pytest +setuptools +wheel +# TODO: update the necessary requirements to develop this package \ No newline at end of file diff --git a/files/templates/newPackageTemplate/package_name/__init__.py b/files/templates/newPackageTemplate/package_name/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/files/templates/newPackageTemplate/package_name/__main__.py b/files/templates/newPackageTemplate/package_name/__main__.py new file mode 100644 index 0000000..50e55f0 --- /dev/null +++ b/files/templates/newPackageTemplate/package_name/__main__.py @@ -0,0 +1,9 @@ +# TODO: Update the main function to your needs or remove it. + + +def main() -> None: + print("Start coding in Python today!") + + +if __name__ == "__main__": + main() diff --git a/files/templates/newPackageTemplate/pyproject.toml b/files/templates/newPackageTemplate/pyproject.toml new file mode 100644 index 0000000..57b7bbd --- /dev/null +++ b/files/templates/newPackageTemplate/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "package_name" +version = "1.0.0" +description = "" #TODO: Add a short description of your package +authors = [{name = "Your Name", email = "your@email.com"}] #TODO: Add your name and email +requires-python = ">=3.9" +dynamic = ["optional-dependencies"] + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic] +optional-dependencies = {dev = {file = ["dev-requirements.txt"]}} # add packages for development in the dev-requirements.txt file + +[project.scripts] +# TODO: add your CLI entry points here + + + + diff --git a/files/templates/newPackageTemplate/tests/test_package_name.py b/files/templates/newPackageTemplate/tests/test_package_name.py new file mode 100644 index 0000000..b622f4e --- /dev/null +++ b/files/templates/newPackageTemplate/tests/test_package_name.py @@ -0,0 +1 @@ +# TODO: Write tests for your package using pytest diff --git a/images/environment-managers-quick-start.png b/images/environment-managers-quick-start.png new file mode 100644 index 0000000..c36f1f3 Binary files /dev/null and b/images/environment-managers-quick-start.png differ diff --git a/images/extension_relationships.png b/images/extension_relationships.png new file mode 100644 index 0000000..f90ea73 Binary files /dev/null and b/images/extension_relationships.png differ diff --git a/images/python-envs-overview.gif b/images/python-envs-overview.gif new file mode 100644 index 0000000..591655f Binary files /dev/null and b/images/python-envs-overview.gif differ diff --git a/images/trust_relationships.png b/images/trust_relationships.png new file mode 100644 index 0000000..3c98b7e Binary files /dev/null and b/images/trust_relationships.png differ diff --git a/package-lock.json b/package-lock.json index 9218a5f..b42f9b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "vscode-python-envs", - "version": "0.0.1", + "version": "1.11.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-python-envs", - "version": "0.0.1", + "version": "1.11.0", "dependencies": { "@iarna/toml": "^2.2.5", + "@vscode/extension-telemetry": "^0.9.7", "@vscode/test-cli": "^0.0.10", + "dotenv": "^16.4.5", "fs-extra": "^11.2.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", @@ -20,23 +22,27 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "20.2.5", + "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.99.0", "@types/which": "^3.0.4", - "@typescript-eslint/eslint-plugin": "^5.59.8", - "@typescript-eslint/parser": "^5.59.8", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "@vscode/test-electron": "^2.3.2", "@vscode/vsce": "^2.24.0", - "eslint": "^8.41.0", + "eslint": "^9.15.0", "glob": "^8.1.0", - "mocha": "^10.2.0", + "mocha": "^10.8.2", + "sinon": "^19.0.2", "ts-loader": "^9.4.3", + "ts-mockito": "^2.6.1", + "typemoq": "^2.1.0", "typescript": "^5.1.3", "webpack": "^5.85.0", "webpack-cli": "^5.1.1" }, "engines": { - "vscode": "^1.93.0-20240910" + "vscode": "^1.104.0-20250815" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -63,39 +69,86 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -103,33 +156,85 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { @@ -145,11 +250,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@iarna/toml": { "version": "2.2.5", @@ -309,11 +422,136 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@microsoft/1ds-core-js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "node_modules/@microsoft/1ds-post-js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "4.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "node_modules/@microsoft/applicationinsights-web-basic": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "license": "MIT", + "dependencies": { + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + }, + "peerDependencies": { + "tslib": ">= 1.0.0" + } + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, + "node_modules/@nevware21/ts-async": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.3.tgz", + "integrity": "sha512-UsF7eerLsVfid7iV1oXF80qXBwHNBeqSqfh/nPZgirRU1MACmSsj83EZKS2ViFHVfSGG6WIuXMGBP6KciXfYhA==", + "license": "MIT", + "dependencies": { + "@nevware21/ts-utils": ">= 0.11.5 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.5.tgz", + "integrity": "sha512-7nIzWKR50mf3htOg53kwPLqD5iJaRfVyBvb1NJhlIncyP1WzK8vAQbU9rqIsRtv7td1CnqspdP6IWNEjOjaeug==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -327,6 +565,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -336,6 +575,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -353,6 +593,55 @@ "node": ">=14" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + } + }, + "node_modules/@sinonjs/samsam/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -363,10 +652,11 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/fs-extra": { "version": "11.0.4", @@ -425,11 +715,22 @@ "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", "dev": true }, - "node_modules/@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", - "dev": true + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/stack-trace": { "version": "0.0.29", @@ -438,10 +739,11 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", - "dev": true + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", + "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/which": { "version": "3.0.4", @@ -450,32 +752,32 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -484,25 +786,27 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -511,16 +815,17 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -528,25 +833,26 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -555,12 +861,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -568,21 +875,23 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -594,54 +903,104 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vscode/extension-telemetry": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", + "integrity": "sha512-2GQbcfDUTg0QC1v0HefkHNwYrE5LYKzS3Zb0+uA6Qn1MBDzgiSh23ddOZF/JRqhqBFOG0mE70XslKSGQ5v9KwQ==", + "license": "MIT", + "dependencies": { + "@microsoft/1ds-core-js": "^4.3.0", + "@microsoft/1ds-post-js": "^4.3.0", + "@microsoft/applicationinsights-web-basic": "^3.3.0" + }, + "engines": { + "vscode": "^1.75.0" + } }, "node_modules/@vscode/test-cli": { "version": "0.0.10", @@ -666,9 +1025,10 @@ } }, "node_modules/@vscode/test-cli/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -1110,10 +1470,11 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1135,6 +1496,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1177,9 +1539,10 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1223,15 +1586,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/azure-devops-node-api": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", @@ -1310,9 +1664,10 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1492,6 +1847,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1633,6 +1989,14 @@ "node": ">=6.0" } }, + "node_modules/circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "deprecated": "CircularJSON is in maintenance only, flatted is its successor.", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -1702,9 +2066,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1762,11 +2126,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1848,37 +2213,14 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1934,6 +2276,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2043,58 +2397,64 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-scope": { @@ -2123,16 +2483,30 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2143,22 +2517,37 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2220,6 +2609,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -2254,6 +2644,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2270,6 +2661,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2303,6 +2695,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -2317,15 +2710,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -2363,24 +2757,25 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { "version": "3.3.0", @@ -2515,9 +2910,10 @@ "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2534,35 +2930,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2755,10 +3129,11 @@ "dev": true }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2886,15 +3261,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -3040,7 +3406,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -3089,6 +3456,13 @@ "setimmediate": "^1.0.5" } }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true, + "license": "MIT" + }, "node_modules/keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", @@ -3106,6 +3480,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -3182,6 +3557,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3270,6 +3659,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -3370,30 +3760,31 @@ "optional": true }, "node_modules/mocha": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", - "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -3404,17 +3795,19 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -3422,11 +3815,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/mocha/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -3442,9 +3830,10 @@ } }, "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -3465,18 +3854,26 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node_modules/node-abi": { "version": "3.56.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", @@ -3610,6 +4007,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -3710,13 +4108,14 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=16" } }, "node_modules/pend": { @@ -3806,6 +4205,17 @@ "node": ">=8" } }, + "node_modules/postinstall-build": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz", + "integrity": "sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==", + "deprecated": "postinstall-build's behavior is now built into npm! You should migrate off of postinstall-build and use the new `prepare` lifecycle script with npm 5.0.0 or greater.", + "dev": true, + "license": "MIT", + "bin": { + "postinstall-build": "cli.js" + } + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -3901,7 +4311,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", @@ -4038,6 +4449,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -4047,46 +4459,12 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4106,6 +4484,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -4154,9 +4533,10 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -4291,13 +4671,33 @@ "simple-concat": "^1.0.0" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=0.3.1" } }, "node_modules/source-map": { @@ -4438,9 +4838,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "optional": true, "dependencies": { @@ -4534,15 +4934,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -4576,16 +4967,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", "dev": true, "engines": { "node": ">=14.14" @@ -4602,6 +4987,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", + "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -4622,26 +5020,21 @@ "webpack": "^5.0.0" } }, + "node_modules/ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.5" + } + }, "node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } + "peer": true }, "node_modules/tunnel": { "version": "0.0.6", @@ -4677,16 +5070,14 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/typed-rest-client": { @@ -4700,6 +5091,22 @@ "underscore": "^1.12.1" } }, + "node_modules/typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -4962,9 +5369,10 @@ "dev": true }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -5057,9 +5465,10 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", "engines": { "node": ">=10" } @@ -5128,30 +5537,59 @@ "dev": true }, "@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "requires": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" } }, "@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true }, + "@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } + }, + "@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "requires": { + "@eslint/core": "^0.16.0" + } + }, + "@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.15" + } + }, "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -5160,20 +5598,49 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true + }, + "@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + } + }, + "@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true + }, + "@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "requires": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "dependencies": { + "@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true + } } }, "@humanwhocodes/module-importer": { @@ -5182,10 +5649,10 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true }, "@iarna/toml": { @@ -5302,6 +5769,108 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@microsoft/1ds-core-js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-4.3.4.tgz", + "integrity": "sha512-3gbDUQgAO8EoyQTNcAEkxpuPnioC0May13P1l1l0NKZ128L9Ts/sj8QsfwCRTjHz0HThlA+4FptcAJXNYUy3rg==", + "requires": { + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "@microsoft/1ds-post-js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-4.3.4.tgz", + "integrity": "sha512-nlKjWricDj0Tn68Dt0P8lX9a+X7LYrqJ6/iSfQwMfDhRIGLqW+wxx8gxS+iGWC/oc8zMQAeiZaemUpCwQcwpRQ==", + "requires": { + "@microsoft/1ds-core-js": "4.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "@microsoft/applicationinsights-channel-js": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-channel-js/-/applicationinsights-channel-js-3.3.4.tgz", + "integrity": "sha512-Z4nrxYwGKP9iyrYtm7iPQXVOFy4FsEsX0nDKkAi96Qpgw+vEh6NH4ORxMMuES0EollBQ3faJyvYCwckuCVIj0g==", + "requires": { + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "@microsoft/applicationinsights-common": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.3.4.tgz", + "integrity": "sha512-4ms16MlIvcP4WiUPqopifNxcWCcrXQJ2ADAK/75uok2mNQe6ZNRsqb/P+pvhUxc8A5HRlvoXPP1ptDSN5Girgw==", + "requires": { + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "@microsoft/applicationinsights-core-js": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.3.4.tgz", + "integrity": "sha512-MummANF0mgKIkdvVvfmHQTBliK114IZLRhTL0X0Ep+zjDwWMHqYZgew0nlFKAl6ggu42abPZFK5afpE7qjtYJA==", + "requires": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 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/applicationinsights-web-basic": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-basic/-/applicationinsights-web-basic-3.3.4.tgz", + "integrity": "sha512-OpEPXr8vU/t/M8T9jvWJzJx/pCyygIiR1nGM/2PTde0wn7anl71Gxl5fWol7K/WwFEORNjkL3CEyWOyDc+28AA==", + "requires": { + "@microsoft/applicationinsights-channel-js": "3.3.4", + "@microsoft/applicationinsights-common": "3.3.4", + "@microsoft/applicationinsights-core-js": "3.3.4", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.3", + "@nevware21/ts-async": ">= 0.5.2 < 2.x", + "@nevware21/ts-utils": ">= 0.11.3 < 2.x" + } + }, + "@microsoft/dynamicproto-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-2.0.3.tgz", + "integrity": "sha512-JTWTU80rMy3mdxOjjpaiDQsTLZ6YSGGqsjURsY6AUQtIj0udlF/jYmhdLZu8693ZIC0T1IwYnFa0+QeiMnziBA==", + "requires": { + "@nevware21/ts-utils": ">= 0.10.4 < 2.x" + } + }, + "@nevware21/ts-async": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.5.3.tgz", + "integrity": "sha512-UsF7eerLsVfid7iV1oXF80qXBwHNBeqSqfh/nPZgirRU1MACmSsj83EZKS2ViFHVfSGG6WIuXMGBP6KciXfYhA==", + "requires": { + "@nevware21/ts-utils": ">= 0.11.5 < 2.x" + } + }, + "@nevware21/ts-utils": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.11.5.tgz", + "integrity": "sha512-7nIzWKR50mf3htOg53kwPLqD5iJaRfVyBvb1NJhlIncyP1WzK8vAQbU9rqIsRtv7td1CnqspdP6IWNEjOjaeug==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5334,6 +5903,49 @@ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "optional": true }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "@sinonjs/samsam": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", + "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "lodash.get": "^4.4.2", + "type-detect": "^4.1.0" + }, + "dependencies": { + "type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true + } + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -5341,9 +5953,9 @@ "dev": true }, "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "@types/fs-extra": { @@ -5403,10 +6015,19 @@ "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==", "dev": true }, - "@types/semver": { - "version": "7.5.6", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", + "@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "requires": { + "@types/sinonjs__fake-timers": "*" + } + }, + "@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", "dev": true }, "@types/stack-trace": { @@ -5416,9 +6037,9 @@ "dev": true }, "@types/vscode": { - "version": "1.93.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.93.0.tgz", - "integrity": "sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==", + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", + "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", "dev": true }, "@types/which": { @@ -5428,109 +6049,138 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz", + "integrity": "sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/type-utils": "8.16.0", + "@typescript-eslint/utils": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.16.0.tgz", + "integrity": "sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz", + "integrity": "sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0" } }, "@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz", + "integrity": "sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "8.16.0", + "@typescript-eslint/utils": "8.16.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.16.0.tgz", + "integrity": "sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz", + "integrity": "sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/visitor-keys": "8.16.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.16.0.tgz", + "integrity": "sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.16.0", + "@typescript-eslint/types": "8.16.0", + "@typescript-eslint/typescript-estree": "8.16.0" } }, "@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz", + "integrity": "sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "8.16.0", + "eslint-visitor-keys": "^4.2.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true + } } }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "@vscode/extension-telemetry": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@vscode/extension-telemetry/-/extension-telemetry-0.9.7.tgz", + "integrity": "sha512-2GQbcfDUTg0QC1v0HefkHNwYrE5LYKzS3Zb0+uA6Qn1MBDzgiSh23ddOZF/JRqhqBFOG0mE70XslKSGQ5v9KwQ==", + "requires": { + "@microsoft/1ds-core-js": "^4.3.0", + "@microsoft/1ds-post-js": "^4.3.0", + "@microsoft/applicationinsights-web-basic": "^3.3.0" + } }, "@vscode/test-cli": { "version": "0.0.10", @@ -5549,9 +6199,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } @@ -5912,9 +6562,9 @@ "dev": true }, "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true }, "acorn-import-attributes": { @@ -5960,9 +6610,9 @@ "requires": {} }, "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==" }, "ansi-regex": { "version": "5.0.1", @@ -5991,12 +6641,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, "azure-devops-node-api": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", @@ -6057,9 +6701,9 @@ "dev": true }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6268,6 +6912,12 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -6331,9 +6981,9 @@ "dev": true }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6375,11 +7025,11 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -6429,27 +7079,9 @@ "optional": true }, "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" }, "dom-serializer": { "version": "2.0.0", @@ -6488,6 +7120,11 @@ "domhandler": "^5.0.3" } }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6567,61 +7204,64 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "dependencies": { "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + }, "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -6647,14 +7287,22 @@ "dev": true }, "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "requires": { - "acorn": "^8.9.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.1" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true + } } }, "esquery": { @@ -6783,12 +7431,12 @@ } }, "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" } }, "fill-range": { @@ -6814,20 +7462,19 @@ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" }, "flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "requires": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" } }, "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, "foreground-child": { @@ -6911,9 +7558,9 @@ }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } @@ -6944,27 +7591,10 @@ "dev": true }, "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true }, "gopd": { "version": "1.0.1", @@ -7093,9 +7723,9 @@ "dev": true }, "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "requires": { "parent-module": "^1.0.0", @@ -7185,12 +7815,6 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -7341,6 +7965,12 @@ "setimmediate": "^1.0.5" } }, + "just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, "keytar": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", @@ -7415,6 +8045,18 @@ "p-locate": "^5.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -7551,53 +8193,48 @@ "optional": true }, "mocha": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", - "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "requires": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "dependencies": { "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "requires": { "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "requires": { "brace-expansion": "^2.0.1" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -7609,9 +8246,9 @@ } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "mute-stream": { "version": "0.0.8", @@ -7632,18 +8269,25 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "nise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.1", + "@sinonjs/text-encoding": "^0.7.3", + "just-extend": "^6.2.0", + "path-to-regexp": "^8.1.0" + } + }, "node-abi": { "version": "3.56.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.56.0.tgz", @@ -7824,10 +8468,10 @@ } } }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", "dev": true }, "pend": { @@ -7895,6 +8539,12 @@ } } }, + "postinstall-build": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.3.tgz", + "integrity": "sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg==", + "dev": true + }, "prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -8076,31 +8726,6 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -8141,9 +8766,9 @@ } }, "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "requires": { "randombytes": "^2.1.0" } @@ -8226,11 +8851,27 @@ "simple-concat": "^1.0.0" } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "sinon": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1", + "@sinonjs/fake-timers": "^13.0.2", + "@sinonjs/samsam": "^8.0.1", + "diff": "^7.0.0", + "nise": "^6.1.1", + "supports-color": "^7.2.0" + }, + "dependencies": { + "diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true + } + } }, "source-map": { "version": "0.7.4", @@ -8331,9 +8972,9 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dev": true, "optional": true, "requires": { @@ -8394,17 +9035,6 @@ "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.1", "terser": "^5.26.0" - }, - "dependencies": { - "serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - } } }, "test-exclude": { @@ -8432,16 +9062,10 @@ } } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "tmp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", - "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", "dev": true }, "to-regex-range": { @@ -8452,6 +9076,13 @@ "is-number": "^7.0.0" } }, + "ts-api-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.2.tgz", + "integrity": "sha512-ZF5gQIQa/UmzfvxbHZI3JXN0/Jt+vnAfAviNRAMc491laiK6YCLpCW9ft8oaCRFOTxCZtUTE6XB0ZQAe3olntw==", + "dev": true, + "requires": {} + }, "ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -8465,20 +9096,20 @@ "source-map": "^0.7.4" } }, + "ts-mockito": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.6.1.tgz", + "integrity": "sha512-qU9m/oEBQrKq5hwfbJ7MgmVN5Gu6lFnIGWvpxSjrqq6YYEVv+RwVFWySbZMBgazsWqv6ctAyVBpo9TmAxnOEKw==", + "dev": true, + "requires": { + "lodash": "^4.17.5" + } + }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } + "peer": true }, "tunnel": { "version": "0.0.6", @@ -8505,10 +9136,10 @@ "prelude-ls": "^1.2.1" } }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "typed-rest-client": { @@ -8522,6 +9153,17 @@ "underscore": "^1.12.1" } }, + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + } + }, "typescript": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", @@ -8693,9 +9335,9 @@ "dev": true }, "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==" + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==" }, "wrap-ansi": { "version": "7.0.0", @@ -8763,9 +9405,9 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==" + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" }, "yargs-unparser": { "version": "2.0.0", diff --git a/package.json b/package.json index 4cd05cb..0ae1c9a 100644 --- a/package.json +++ b/package.json @@ -1,200 +1,307 @@ { "name": "vscode-python-envs", - "displayName": "Python Environment Manager", + "displayName": "Python Environments", "description": "Provides a unified python environment experience", - "version": "0.0.1", + "version": "1.11.0", "publisher": "ms-python", + "preview": true, "engines": { - "vscode": "^1.93.0-20240910" + "vscode": "^1.104.0-20250815" }, "categories": [ "Other" ], + "enabledApiProposals": [ + "terminalShellEnv" + ], + "capabilities": { + "untrustedWorkspaces": { + "supported": false, + "description": "This extension doesn't support untrusted workspaces." + }, + "virtualWorkspaces": { + "supported": false, + "description": "This extension doesn't support virtual workspaces." + } + }, "activationEvents": [ "onLanguage:python" ], - "homepage": "https://github.com/karthiknadig/vscode-python-envs", + "homepage": "https://github.com/microsoft/vscode-python-environments", "repository": { "type": "git", - "url": "https://github.com/karthiknadig/vscode-python-envs.git" + "url": "https://github.com/microsoft/vscode-python-environments.git" }, "bugs": { - "url": "https://github.com/karthiknadig/vscode-python-envs/issues" + "url": "https://github.com/microsoft/vscode-python-environments/issues" }, - "extensionDependencies": [ - "ms-python.python" - ], "main": "./dist/extension.js", + "l10n": "./l10n", "icon": "icon.png", "contributes": { "configuration": { "properties": { "python-envs.defaultEnvManager": { "type": "string", - "description": "The default environment manager to use for creating and managing environments", + "description": "%python-envs.defaultEnvManager.description%", "default": "ms-python.python:venv", "scope": "window" }, "python-envs.defaultPackageManager": { "type": "string", - "description": "The default package manager to use for installing and managing packages", + "description": "%python-envs.defaultPackageManager.description%", "default": "ms-python.python:pip", "scope": "window" }, "python-envs.pythonProjects": { "type": "array", "default": [], - "description": "The list of python workspaces", - "scope": "window", + "description": "%python-envs.pythonProjects.description%", + "scope": "resource", "items": { "type": "object", "properties": { "path": { "type": "string", - "description": "The path to the workspace folder or a file" + "description": "%python-envs.pythonProjects.path.description%" }, "envManager": { "type": "string", - "description": "The environment manager to use for this workspace", + "description": "%python-envs.pythonProjects.envManager.description%", "default": "ms-python.python:venv" }, "packageManager": { "type": "string", - "description": "The package manager to use for this workspace", + "description": "%python-envs.pythonProjects.packageManager.description%", "default": "ms-python.python:pip" } } } + }, + "python-envs.terminal.showActivateButton": { + "type": "boolean", + "description": "%python-envs.terminal.showActivateButton.description%", + "default": false, + "scope": "machine", + "tags": [ + "onExP", + "preview" + ] + }, + "python-envs.terminal.autoActivationType": { + "type": "string", + "markdownDescription": "%python-envs.terminal.autoActivationType.description%", + "default": "command", + "enum": [ + "command", + "shellStartup", + "off" + ], + "markdownEnumDescriptions": [ + "%python-envs.terminal.autoActivationType.command%", + "%python-envs.terminal.autoActivationType.shellStartup%", + "%python-envs.terminal.autoActivationType.off%" + ], + "scope": "machine" + }, + "python.terminal.useEnvFile": { + "type": "boolean", + "description": "%python-envs.terminal.useEnvFile.description%", + "default": false, + "scope": "resource" + }, + "python-env.globalSearchPaths": { + "type": "array", + "description": "%python-env.globalSearchPaths.description%", + "default": [], + "scope": "machine", + "items": { + "type": "string" + } + }, + "python-env.workspaceSearchPaths": { + "type": "array", + "description": "%python-env.workspaceSearchPaths.description%", + "default": [], + "scope": "resource", + "items": { + "type": "string" + } } } }, "commands": [ { "command": "python-envs.setEnvManager", - "title": "Set Environment Manager", + "title": "%python-envs.setEnvManager.title%", "category": "Python", - "icon": "$(gear)" + "icon": "$(gear)", + "when": "config.python.useEnvironmentsExtension != false" }, { "command": "python-envs.setPkgManager", - "title": "Set Package Manager", + "title": "%python-envs.setPkgManager.title%", "category": "Python", "icon": "$(package)" }, { "command": "python-envs.addPythonProject", - "title": "Add Python Workspace", + "title": "%python-envs.addPythonProject.title%", + "category": "Python", + "icon": "$(new-folder)" + }, + { + "command": "python-envs.addPythonProjectGivenResource", + "title": "%python-envs.addPythonProjectGivenResource.title%", "category": "Python", "icon": "$(new-folder)" }, { "command": "python-envs.removePythonProject", - "title": "Remove Python Workspace", + "title": "%python-envs.removePythonProject.title%", "category": "Python", "icon": "$(remove)" }, { "command": "python-envs.create", - "title": "Create Environment", + "title": "%python-envs.create.title%", "category": "Python", "icon": "$(add)" }, { "command": "python-envs.createAny", - "title": "Create Environment", + "title": "%python-envs.createAny.title%", "category": "Python", "icon": "$(add)" }, { "command": "python-envs.set", - "title": "Set Workspace Environment", + "title": "%python-envs.set.title%", "category": "Python", "icon": "$(check)" }, { "command": "python-envs.setEnv", - "title": "Set As Workspace Environment", + "title": "%python-envs.setEnv.title%", "category": "Python", "icon": "$(check)" }, - { - "command": "python-envs.reset", - "title": "Reset Environment Selection to Default", - "shortTitle": "Reset Environment", - "category": "Python", - "icon": "$(sync)" - }, { "command": "python-envs.remove", - "title": "Delete Environment", + "title": "%python-envs.remove.title%", "category": "Python", "icon": "$(remove)" }, { "command": "python-envs.refreshAllManagers", - "title": "Refresh All Environment Managers", + "title": "%python-envs.refreshAllManagers.title%", "shortTitle": "Refresh All", "category": "Python", "icon": "$(refresh)" }, - { - "command": "python-envs.refreshManager", - "title": "Refresh Environments List", - "category": "Python", - "icon": "$(refresh)" - }, { "command": "python-envs.refreshPackages", - "title": "Refresh Packages List", + "title": "%python-envs.refreshPackages.title%", "category": "Python", "icon": "$(refresh)" }, { "command": "python-envs.packages", - "title": "Install or Remove Packages", - "shortTitle": "Modify Packages", + "title": "%python-envs.packages.title%", "category": "Python", "icon": "$(package)" }, { "command": "python-envs.clearCache", - "title": "Clear Cache", + "title": "%python-envs.clearCache.title%", "category": "Python", "icon": "$(trash)" }, { "command": "python-envs.runInTerminal", - "title": "Run in Terminal", + "title": "%python-envs.runInTerminal.title%", "category": "Python Envs", "icon": "$(play)" }, { "command": "python-envs.createTerminal", - "title": "Create Python Terminal", + "title": "%python-envs.createTerminal.title%", "category": "Python Envs", "icon": "$(terminal)" }, + { + "command": "python-envs.createNewProjectFromTemplate", + "title": "%python-envs.createNewProjectFromTemplate.title%", + "category": "Python Envs", + "icon": "$(play)" + }, { "command": "python-envs.runAsTask", - "title": "Run as Task", + "title": "%python-envs.runAsTask.title%", "category": "Python Envs", "icon": "$(play)" + }, + { + "command": "python-envs.terminal.activate", + "title": "%python-envs.terminal.activate.title%", + "category": "Python Envs", + "icon": "$(python)" + }, + { + "command": "python-envs.terminal.deactivate", + "title": "%python-envs.terminal.deactivate.title%", + "category": "Python Envs", + "icon": "$(circle-slash)" + }, + { + "command": "python-envs.uninstallPackage", + "title": "%python-envs.uninstallPackage.title%", + "category": "Python Envs", + "icon": "$(trash)" + }, + { + "command": "python-envs.copyEnvPath", + "title": "%python-envs.copyEnvPath.title%", + "category": "Python Envs", + "icon": "$(copy)" + }, + { + "command": "python-envs.copyProjectPath", + "title": "%python-envs.copyProjectPath.title%", + "category": "Python Envs", + "icon": "$(copy)" + }, + { + "command": "python-envs.terminal.revertStartupScriptChanges", + "title": "%python-envs.terminal.revertStartupScriptChanges.title%", + "category": "Python Envs", + "icon": "$(discard)" + }, + { + "command": "python-envs.reportIssue", + "title": "%python-envs.reportIssue.title%", + "category": "Python Environments" + }, + { + "command": "python-envs.revealProjectInExplorer", + "title": "%python-envs.revealProjectInExplorer.title%", + "category": "Python Envs", + "icon": "$(folder-opened)" + }, + { + "command": "python-envs.runPetInTerminal", + "title": "%python-envs.runPetInTerminal.title%", + "category": "Python", + "icon": "$(terminal)", + "when": "config.python.useEnvironmentsExtension != false" } ], "menus": { "commandPalette": [ - { - "command": "python-envs.packages", - "when": "false" - }, { "command": "python-envs.refreshAllManagers", "when": "false" }, - { - "command": "python-envs.refreshManager", - "when": "false" - }, { "command": "python-envs.refreshPackages", "when": "false" @@ -208,17 +315,33 @@ "when": "false" }, { - "command": "python-envs.reset", + "command": "python-envs.remove", "when": "false" }, { - "command": "python-envs.remove", + "command": "python-envs.addPythonProject", "when": "false" }, { - "command": "python-envs.addPythonProject", + "command": "python-envs.addPythonProjectGivenResource", "when": "false" }, + { + "command": "python-envs.setEnvManager", + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.packages", + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.set", + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.setPkgManager", + "when": "config.python.useEnvironmentsExtension != false" + }, { "command": "python-envs.removePythonProject", "when": "false" @@ -233,18 +356,58 @@ }, { "command": "python-envs.runAsTask", - "when": "true" + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.terminal.activate", + "when": "pythonTerminalActivation" + }, + { + "command": "python-envs.terminal.deactivate", + "when": "pythonTerminalActivation" + }, + { + "command": "python-envs.uninstallPackage", + "when": "false" + }, + { + "command": "python-envs.copyEnvPath", + "when": "false" + }, + { + "command": "python-envs.copyProjectPath", + "when": "false" + }, + { + "command": "python-envs.createAny", + "when": "false" + }, + { + "command": "python-envs.revealProjectInExplorer", + "when": "false" + }, + { + "command": "python-envs.createNewProjectFromTemplate", + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.terminal.revertStartupScriptChanges", + "when": "config.python.useEnvironmentsExtension != false" + }, + { + "command": "python-envs.reportIssue", + "when": "config.python.useEnvironmentsExtension != false" } ], "view/item/context": [ { "command": "python-envs.create", "group": "inline", - "when": "view == env-managers && viewItem =~ /.*pythonEnvManager.*-create.*/" + "when": "view == env-managers && viewItem =~ /.*pythonEnvManager.*;create;.*/" }, { "command": "python-envs.remove", - "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*-remove.*/" + "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*;remove;.*/" }, { "command": "python-envs.setEnv", @@ -252,52 +415,65 @@ "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/" }, { - "command": "python-envs.refreshManager", + "command": "python-envs.createTerminal", "group": "inline", - "when": "view == env-managers && viewItem =~ /.*pythonEnvManager.*/" + "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*;activatable;.*/" }, { - "command": "python-envs.createTerminal", - "group": "inline", + "command": "python-envs.refreshPackages", "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/" }, { - "command": "python-envs.refreshPackages", + "command": "python-envs.packages", "group": "inline", - "when": "view == env-managers && viewItem == python-package-root" + "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/" }, { - "command": "python-envs.packages", + "command": "python-envs.copyEnvPath", "group": "inline", "when": "view == env-managers && viewItem =~ /.*pythonEnvironment.*/" }, { - "command": "python-envs.packages", + "command": "python-envs.uninstallPackage", "group": "inline", - "when": "view == python-projects && viewItem == python-env" + "when": "view == env-managers && viewItem == python-package" }, { - "command": "python-envs.refreshPackages", + "command": "python-envs.copyEnvPath", "group": "inline", - "when": "view == python-projects && viewItem == python-package-root" + "when": "view == python-projects && viewItem == python-env" + }, + { + "command": "python-envs.remove", + "when": "view == python-projects && viewItem == python-env" }, { "command": "python-envs.removePythonProject", - "when": "view == python-projects && viewItem == python-workspace" + "when": "view == python-projects && viewItem == python-workspace-removable" }, { "command": "python-envs.set", "group": "inline", - "when": "view == python-projects && viewItem == python-workspace" + "when": "view == python-projects && viewItem =~ /.*python-workspace.*/" }, { - "command": "python-envs.reset", - "when": "view == python-projects && viewItem == python-workspace" + "command": "python-envs.createTerminal", + "group": "inline", + "when": "view == python-projects && viewItem =~ /.*python-workspace.*/" }, { - "command": "python-envs.createTerminal", + "command": "python-envs.copyProjectPath", "group": "inline", - "when": "view == python-projects && viewItem == python-workspace" + "when": "view == python-projects && viewItem =~ /.*python-workspace.*/" + }, + { + "command": "python-envs.revealProjectInExplorer", + "when": "view == python-projects && viewItem =~ /.*python-workspace.*/" + }, + { + "command": "python-envs.uninstallPackage", + "group": "inline", + "when": "view == python-projects && viewItem == python-package" } ], "view/title": [ @@ -315,50 +491,85 @@ "command": "python-envs.refreshAllManagers", "group": "navigation", "when": "view == env-managers" + }, + { + "command": "python-envs.terminal.activate", + "group": "navigation", + "when": "view == terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && !pythonTerminalActivated" + }, + { + "command": "python-envs.terminal.deactivate", + "group": "navigation", + "when": "view == terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && pythonTerminalActivated" } ], "explorer/context": [ { - "command": "python-envs.addPythonProject", + "command": "python-envs.addPythonProjectGivenResource", "group": "inline", "when": "explorerViewletVisible && explorerResourceIsFolder" }, { - "command": "python-envs.addPythonProject", + "command": "python-envs.addPythonProjectGivenResource", "group": "inline", "when": "explorerViewletVisible && resourceExtname == .py" } ], + "editor/title": [ + { + "command": "python-envs.terminal.activate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && !pythonTerminalActivated" + }, + { + "command": "python-envs.terminal.deactivate", + "group": "navigation", + "when": "resourceScheme == vscode-terminal && config.python-envs.terminal.showActivateButton && pythonTerminalActivation && pythonTerminalActivated" + } + ], "editor/title/run": [ { "command": "python-envs.runAsTask", "group": "Python", "when": "editorLangId == python" } + ], + "terminal/title/context": [ + { + "command": "python-envs.terminal.activate", + "when": "pythonTerminalActivation && !pythonTerminalActivated" + }, + { + "command": "python-envs.terminal.deactivate", + "when": "pythonTerminalActivation && pythonTerminalActivated" + } ] }, "viewsContainers": { "activitybar": [ { - "id": "python-environments", - "title": "Python Environments", - "icon": "files/logo.svg" + "id": "python", + "title": "Python", + "icon": "files/logo.svg", + "when": "config.python.useEnvironmentsExtension != false" } ] }, "views": { - "python-environments": [ + "python": [ { "id": "python-projects", "name": "Python Projects", "icon": "files/logo.svg", - "contextualTitle": "Workspace Environment" + "contextualTitle": "Python Projects", + "when": "config.python.useEnvironmentsExtension != false" }, { "id": "env-managers", "name": "Environment Managers", "icon": "files/logo.svg", - "contextualTitle": "Environment Manager" + "contextualTitle": "Environment Managers", + "when": "config.python.useEnvironmentsExtension != false" } ] }, @@ -375,9 +586,9 @@ "package": "webpack --mode production --devtool source-map --config ./webpack.config.js", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", - "pretest": "npm run compile-tests && npm run compile && npm run lint", - "lint": "eslint src --ext ts", - "test": "node ./out/test/runTest.js", + "pretest": "npm run compile-tests && npm run compile", + "lint": "eslint --config=eslint.config.mjs src", + "unittest": "mocha --config=./build/.mocha.unittests.json", "vsce-package": "vsce package -o ms-python-envs-insiders.vsix" }, "devDependencies": { @@ -385,24 +596,30 @@ "@types/glob": "^8.1.0", "@types/mocha": "^10.0.1", "@types/node": "20.2.5", + "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", - "@types/vscode": "^1.93.0", + "@types/vscode": "^1.99.0", "@types/which": "^3.0.4", - "@typescript-eslint/eslint-plugin": "^5.59.8", - "@typescript-eslint/parser": "^5.59.8", + "@typescript-eslint/eslint-plugin": "^8.16.0", + "@typescript-eslint/parser": "^8.16.0", "@vscode/test-electron": "^2.3.2", "@vscode/vsce": "^2.24.0", - "eslint": "^8.41.0", + "eslint": "^9.15.0", "glob": "^8.1.0", - "mocha": "^10.2.0", + "mocha": "^10.8.2", + "sinon": "^19.0.2", "ts-loader": "^9.4.3", + "ts-mockito": "^2.6.1", + "typemoq": "^2.1.0", "typescript": "^5.1.3", "webpack": "^5.85.0", "webpack-cli": "^5.1.1" }, "dependencies": { "@iarna/toml": "^2.2.5", + "@vscode/extension-telemetry": "^0.9.7", "@vscode/test-cli": "^0.0.10", + "dotenv": "^16.4.5", "fs-extra": "^11.2.0", "stack-trace": "0.0.10", "vscode-jsonrpc": "^9.0.0-next.5", diff --git a/package.nls.json b/package.nls.json new file mode 100644 index 0000000..38c88ec --- /dev/null +++ b/package.nls.json @@ -0,0 +1,43 @@ +{ + "python-envs.defaultEnvManager.description": "The default environment manager for creating and managing environments.", + "python-envs.defaultPackageManager.description": "The default package manager for installing packages in environments.", + "python-envs.pythonProjects.description": "The list of Python projects.", + "python-envs.pythonProjects.path.description": "The path to a folder or file in the workspace to be treated as a Python project.", + "python-envs.pythonProjects.envManager.description": "The environment manager for creating and managing environments for this project.", + "python-envs.pythonProjects.packageManager.description": "The package manager for managing packages in environments for this project.", + "python-envs.terminal.showActivateButton.description": "Whether to show the 'Activate' button in the terminal menu", + "python-envs.terminal.autoActivationType.description": "Specifies how the extension can activate an environment in a terminal.\n\nUtilizing Shell Startup requires changes to the shell script file and is only enabled for the following shells: zsh, fsh, pwsh, bash, cmd. When set to `command`, any shell can be activated.\n\nThis setting takes precedence over the legacy `python.terminal.activateEnvironment` setting. If this setting is not explicitly set and `python.terminal.activateEnvironment` is set to false, this setting will automatically be set to `off` to preserve your preference.\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\nTo revert changes made during shellStartup, run `Python Envs: Revert Shell Startup Script Changes`.", + "python-envs.terminal.autoActivationType.command": "Activation by executing a command in the terminal.", + "python-envs.terminal.autoActivationType.shellStartup": "Activation by modifying the terminal shell startup script. To use this feature we will need to modify your shell startup scripts.", + "python-envs.terminal.autoActivationType.off": "No automatic activation of environments.", + "python-envs.terminal.useEnvFile.description": "Controls whether environment variables from .env files and python.envFile setting are injected into terminals.", + "python-env.globalSearchPaths.description": "Global search paths for Python environments. Absolute directory paths that are searched at the user level.", + "python-env.workspaceSearchPaths.description": "Workspace search paths for Python environments. Can be absolute paths or relative directory paths searched within the workspace.", + "python-envs.terminal.revertStartupScriptChanges.title": "Revert Shell Startup Script Changes", + "python-envs.reportIssue.title": "Report Issue", + "python-envs.setEnvManager.title": "Set Environment Manager", + "python-envs.setPkgManager.title": "Set Package Manager", + "python-envs.addPythonProject.title": "Add Python Project", + "python-envs.addPythonProjectGivenResource.title": "Add as Python Project", + "python-envs.removePythonProject.title": "Remove Python Project", + "python-envs.copyEnvPath.title": "Copy Environment Path", + "python-envs.copyProjectPath.title": "Copy Project Path", + "python-envs.create.title": "Create Environment", + "python-envs.createAny.title": "Create Environment", + "python-envs.set.title": "Set Project Environment", + "python-envs.setEnv.title": "Set As Project Environment", + "python-envs.remove.title": "Delete Environment", + "python-envs.refreshAllManagers.title": "Refresh All Environment Managers", + "python-envs.refreshPackages.title": "Refresh Packages List", + "python-envs.packages.title": "Manage Packages", + "python-envs.clearCache.title": "Clear Cache", + "python-envs.runInTerminal.title": "Run in Terminal", + "python-envs.createTerminal.title": "Create Python Terminal", + "python-envs.runAsTask.title": "Run as Task", + "python-envs.createNewProjectFromTemplate.title": "Create New Project from Template", + "python-envs.terminal.activate.title": "Activate Environment in Current Terminal", + "python-envs.terminal.deactivate.title": "Deactivate Environment in Current Terminal", + "python-envs.uninstallPackage.title": "Uninstall Package", + "python-envs.revealProjectInExplorer.title": "Reveal Project in Explorer", + "python-envs.runPetInTerminal.title": "Run Python Environment Tool (PET) in Terminal..." +} diff --git a/scripts/list-local-repos.sh b/scripts/list-local-repos.sh new file mode 100644 index 0000000..3d031d9 --- /dev/null +++ b/scripts/list-local-repos.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# List local git repositories under common paths and show remote, last commit, and size. +# Usage: ./scripts/list-local-repos.sh [paths...] + +set -euo pipefail + +PATHS=("/Users/andy/Documents/GitHub" "/Users/andy/Projects" "/Users/andy") +if [ "$#" -gt 0 ]; then + PATHS=("$@") +fi + +printf "Scanning paths: %s\n" "${PATHS[*]}" +echo + +for base in "${PATHS[@]}"; do + if [ ! -d "$base" ]; then + continue + fi + echo "== Path: $base ==" + # find .git directories up to depth 4 + find "$base" -maxdepth 4 -type d -name .git 2>/dev/null | while read -r d; do + repo=$(dirname "$d") + echo "-- Repo: $repo" + if [ -d "$repo" ]; then + (cd "$repo" || exit 0 + echo "Remote(s):" + git remote -v || echo " (no git remotes)" + echo "Size: $(du -sh . 2>/dev/null | cut -f1)" + echo "Last commit:" + git log -1 --pretty=format:"%h %an %ae %ad %s" || echo " (no commits)" + echo "Status:" + git status --porcelain || true + ) + else + echo " (cannot access repo)" + fi + echo + done +done + +echo "Done." diff --git a/src/api.ts b/src/api.ts index 4ea6283..0b60339 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,18 @@ -import { Uri, Disposable, MarkdownString, Event, LogOutputChannel, ThemeIcon } from 'vscode'; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; /** * The path to an icon, or a theme-specific configuration of icons. @@ -34,23 +48,6 @@ export interface PythonCommandRunConfiguration { args?: string[]; } -export enum TerminalShellType { - powershell = 'powershell', - powershellCore = 'powershellCore', - commandPrompt = 'commandPrompt', - gitbash = 'gitbash', - bash = 'bash', - zsh = 'zsh', - ksh = 'ksh', - fish = 'fish', - cshell = 'cshell', - tcshell = 'tshell', - nushell = 'nushell', - wsl = 'wsl', - xonsh = 'xonsh', - unknown = 'unknown', -} - /** * Contains details on how to use a particular python environment * @@ -59,7 +56,7 @@ export enum TerminalShellType { * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: - * - {@link TerminalShellType.unknown} will be used if provided. + * - 'unknown' will be used if provided. * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. @@ -68,7 +65,7 @@ export enum TerminalShellType { * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: - * - {@link TerminalShellType.unknown} will be used if provided. + * - 'unknown' will be used if provided. * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. * @@ -93,11 +90,11 @@ export interface PythonEnvironmentExecutionInfo { /** * Details on how to activate an environment using a shell specific command. * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. - * {@link TerminalShellType.unknown} is used if shell type is not known. - * If {@link TerminalShellType.unknown} is not provided and shell type is not known then + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then * {@link PythonEnvironmentExecutionInfo.activation} if set. */ - shellActivation?: Map; + shellActivation?: Map; /** * Details on how to deactivate an environment. @@ -107,11 +104,11 @@ export interface PythonEnvironmentExecutionInfo { /** * Details on how to deactivate an environment using a shell specific command. * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. - * {@link TerminalShellType.unknown} is used if shell type is not known. - * If {@link TerminalShellType.unknown} is not provided and shell type is not known then + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then * {@link PythonEnvironmentExecutionInfo.deactivation} if set. */ - shellDeactivation?: Map; + shellDeactivation?: Map; } /** @@ -129,6 +126,33 @@ export interface PythonEnvironmentId { managerId: string; } +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + /** * Interface representing information about a Python environment. */ @@ -179,16 +203,20 @@ export interface PythonEnvironmentInfo { readonly iconPath?: IconPath; /** - * Information on how to execute the Python environment. If not provided, {@link PythonEnvironmentApi.resolveEnvironment} will be - * used to to get the details at later point if needed. The recommendation is to fill this in if known. + * Information on how to execute the Python environment. This is required for executing Python code in the environment. */ - readonly execInfo?: PythonEnvironmentExecutionInfo; + readonly execInfo: PythonEnvironmentExecutionInfo; /** * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. */ readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; } /** @@ -205,7 +233,7 @@ export interface PythonEnvironment extends PythonEnvironmentInfo { * Type representing the scope for setting a Python environment. * Can be undefined or a URI. */ -export type SetEnvironmentScope = undefined | Uri; +export type SetEnvironmentScope = undefined | Uri | Uri[]; /** * Type representing the scope for getting a Python environment. @@ -217,7 +245,7 @@ export type GetEnvironmentScope = undefined | Uri; * Type representing the scope for creating a Python environment. * Can be a Python project or 'global'. */ -export type CreateEnvironmentScope = PythonProject | 'global'; +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; /** * The scope for which environments are to be refreshed. * - `undefined`: Search for environments globally and workspaces. @@ -227,7 +255,7 @@ export type RefreshEnvironmentsScope = Uri | undefined; /** * The scope for which environments are required. - * - `undefined`/`"all"`: All environments. + * - `"all"`: All environments. * - `"global"`: Python installations that are usually a base for creating virtual environments. * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. */ @@ -240,7 +268,7 @@ export type DidChangeEnvironmentEventArgs = { /** * The URI of the environment that changed. */ - readonly uri: Uri; + readonly uri: Uri | undefined; /** * The old Python environment before the change. @@ -283,34 +311,29 @@ export type DidChangeEnvironmentsEventArgs = { environment: PythonEnvironment; }[]; -export type PythonIsKnownContext = Uri | string; - /** - * Result of checking if a context is a known Python environment. + * Type representing the context for resolving a Python environment. */ -export interface PythonIsKnownResult { +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { /** - * The confidence level of the result (low, moderate, or high). + * The description of the quick create step. */ - confidence: 'low' | 'moderate' | 'high'; + readonly description: string; /** - * The Python environment match. + * The detail of the quick create step. */ - result: 'unknown' | 'known' | 'canHandle'; + readonly detail?: string; } -/** - * Type representing the context for resolving a Python environment. - */ -export type ResolveEnvironmentContext = PythonEnvironment | Uri; - /** * Interface representing an environment manager. */ export interface EnvironmentManager { /** - * The name of the environment manager. + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ readonly name: string; @@ -320,8 +343,12 @@ export interface EnvironmentManager { readonly displayName?: string; /** - * The preferred package manager ID for the environment manager. - * @example 'ms-python.python:pip' + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' */ readonly preferredPackageManagerId: string; @@ -346,11 +373,18 @@ export interface EnvironmentManager { readonly log?: LogOutputChannel; /** - * Creates a new Python environment within the specified scope. + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. Create should support adding a .gitignore file if it creates a folder within the workspace. If a manager does not support environment creation, do not implement this method; the UI disables "create" options when `this.manager.create === undefined`. * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. * @returns A promise that resolves to the created Python environment, or undefined if creation failed. */ - create?(scope: CreateEnvironmentScope): Promise; + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; /** * Removes the specified Python environment. @@ -412,15 +446,6 @@ export interface EnvironmentManager { */ resolve(context: ResolveEnvironmentContext): Promise; - /** - * Checks if the specified context is a known Python environment. The string/Uri can point to the environment root folder - * or to python executable. It can also be a named environment. - * - * @param context - The URI/string context to check. - * @returns A promise that resolves to the result of the check. - */ - isKnown?(context: PythonIsKnownContext): Promise; - /** * Clears the environment manager's cache. * @@ -539,7 +564,7 @@ export interface DidChangePackagesEventArgs { */ export interface PackageManager { /** - * The name of the package manager. + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). */ name: string; @@ -566,23 +591,15 @@ export interface PackageManager { /** * The log output channel for the package manager. */ - logOutput?: LogOutputChannel; + log?: LogOutputChannel; /** - * Installs packages in the specified Python environment. + * Installs/Uninstall packages in the specified Python environment. * @param environment - The Python environment in which to install packages. - * @param packages - The packages to install. + * @param options - Options for managing packages. * @returns A promise that resolves when the installation is complete. */ - install(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise; - - /** - * Uninstalls packages from the specified Python environment. - * @param environment - The Python environment from which to uninstall packages. - * @param packages - The packages to uninstall, which can be an array of packages or strings. - * @returns A promise that resolves when the uninstall is complete. - */ - uninstall(environment: PythonEnvironment, packages: Package[] | string[]): Promise; + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; /** * Refreshes the package list for the specified Python environment. @@ -598,18 +615,6 @@ export interface PackageManager { */ getPackages(environment: PythonEnvironment): Promise; - /** - * Get a list of installable items for a Python project. - * - * @param environment The Python environment for which to get installable items. - * - * Note: An environment can be used by multiple projects, so the installable items returned. - * should be for the environment. IF you want to do it for a particular project, then you may - * shown a QuickPick to the user to select the project, and filter the installable items based - * on the project. - */ - getInstallable?(environment: PythonEnvironment): Promise; - /** * Event that is fired when packages change. */ @@ -645,11 +650,6 @@ export interface PythonProject { * The tooltip for the Python project, which can be a string or a Markdown string. */ readonly tooltip?: string | MarkdownString; - - /** - * The icon path for the Python project, which can be a string, Uri, or an object with light and dark theme paths. - */ - readonly iconPath?: IconPath; } /** @@ -662,9 +662,14 @@ export interface PythonProjectCreatorOptions { name: string; /** - * Optional path that may be provided as a root for the project. + * Path provided as the root for the project. */ - uri?: Uri; + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; } /** @@ -692,16 +697,20 @@ export interface PythonProjectCreator { readonly tooltip?: string | MarkdownString; /** - * The icon path for the Python project creator, which can be a string, Uri, or an object with light and dark theme paths. + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. */ - readonly iconPath?: IconPath; + create(options?: PythonProjectCreatorOptions): Promise; /** - * Creates a new Python project or projects. - * @param options - Optional parameters for creating the Python project. - * @returns A promise that resolves to a Python project, an array of Python projects, or undefined. + * A flag indicating whether the project creator supports quick create where no user input is required. */ - create(options?: PythonProjectCreatorOptions): Promise; + readonly supportsQuickCreate?: boolean; } /** @@ -719,55 +728,103 @@ export interface DidChangePythonProjectsEventArgs { removed: PythonProject[]; } +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + /** - * Options for package installation. + * Options for creating a Python environment. */ -export interface PackageInstallOptions { +export interface CreateEnvironmentOptions { /** - * Upgrade the packages if it is already installed. + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. */ - upgrade?: boolean; + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; } -export interface Installable { +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { /** - * The display name of the package, requirements, pyproject.toml or any other project file. + * The process ID of the Python process. */ - readonly displayName: string; + readonly pid?: number; /** - * Arguments passed to the package manager to install the package. - * - * @example - * ['debugpy==1.8.7'] for `pip install debugpy==1.8.7`. - * ['--pre', 'debugpy'] for `pip install --pre debugpy`. - * ['-r', 'requirements.txt'] for `pip install -r requirements.txt`. + * The standard input of the Python process. */ - readonly args: string[]; + readonly stdin: NodeJS.WritableStream; /** - * Installable group name, this will be used to group installable items in the UI. - * - * @example - * `Requirements` for any requirements file. - * `Packages` for any package. + * The standard output of the Python process. */ - readonly group?: string; + readonly stdout: NodeJS.ReadableStream; /** - * Path to the requirements, version of the package, or any other project file path. + * The standard error of the Python process. */ - readonly description?: string; + readonly stderr: NodeJS.ReadableStream; /** - * External Uri to the package on pypi or docs. - * @example - * https://pypi.org/project/debugpy/ for `debugpy`. + * Kills the Python process. */ - readonly uri?: Uri; + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; } -export interface PythonEnvironmentManagerApi { +export interface PythonEnvironmentManagerRegistrationApi { /** * Register an environment manager implementation. * @@ -776,7 +833,9 @@ export interface PythonEnvironmentManagerApi { * @see {@link EnvironmentManager} */ registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} +export interface PythonEnvironmentItemApi { /** * Create a Python environment item from the provided environment info. This item is used to interact * with the environment. @@ -786,14 +845,20 @@ export interface PythonEnvironmentManagerApi { * @returns The Python environment. */ createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} +export interface PythonEnvironmentManagementApi { /** * Create a Python environment using environment manager associated with the scope. * * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. * @returns The Python environment created. `undefined` if not created. */ - createEnvironment(scope: CreateEnvironmentScope): Promise; + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; /** * Remove a Python environment. @@ -802,7 +867,9 @@ export interface PythonEnvironmentManagerApi { * @returns A promise that resolves when the environment has been removed. */ removeEnvironment(environment: PythonEnvironment): Promise; +} +export interface PythonEnvironmentsApi { /** * Initiates a refresh of Python environments within the specified scope. * @param scope - The scope within which to search for environments. @@ -823,12 +890,22 @@ export interface PythonEnvironmentManagerApi { */ onDidChangeEnvironments: Event; + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { /** * Sets the current Python environment within the specified scope. * @param scope - The scope within which to set the environment. * @param environment - The Python environment to set. If undefined, the environment is unset. */ - setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): void; + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; /** * Retrieves the current Python environment within the specified scope. @@ -842,17 +919,16 @@ export interface PythonEnvironmentManagerApi { * @see {@link DidChangeEnvironmentEventArgs} */ onDidChangeEnvironment: Event; - - /** - * This method is used to get the details missing from a PythonEnvironment. Like - * {@link PythonEnvironment.execInfo} and other details. - * - * @param context : The PythonEnvironment or Uri for which details are required. - */ - resolveEnvironment(context: ResolveEnvironmentContext): Promise; } -export interface PythonPackageManagerApi { +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { /** * Register a package manager implementation. * @@ -861,24 +937,9 @@ export interface PythonPackageManagerApi { * @see {@link PackageManager} */ registerPackageManager(manager: PackageManager): Disposable; +} - /** - * Install packages into a Python Environment. - * - * @param environment The Python Environment into which packages are to be installed. - * @param packages The packages to install. - * @param options Options for installing packages. - */ - installPackages(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise; - - /** - * Uninstall packages from a Python Environment. - * - * @param environment The Python Environment from which packages are to be uninstalled. - * @param packages The packages to uninstall. - */ - uninstallPackages(environment: PythonEnvironment, packages: PackageInfo[] | string[]): Promise; - +export interface PythonPackageGetterApi { /** * Refresh the list of packages in a Python Environment. * @@ -900,7 +961,9 @@ export interface PythonPackageManagerApi { * @see {@link DidChangePackagesEventArgs} */ onDidChangePackages: Event; +} +export interface PythonPackageItemApi { /** * Create a package item from the provided package info. * @@ -912,6 +975,70 @@ export interface PythonPackageManagerApi { createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; } +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + /** * The API for interacting with Python projects. A project in python is any folder or file that is a contained * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, @@ -919,51 +1046,220 @@ export interface PythonPackageManagerApi { * * By default all `vscode.workspace.workspaceFolders` are treated as projects. */ -export interface PythonProjectApi { +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { /** - * Add a python project or projects to the list of projects. + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. * - * @param projects The project or projects to add. + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. */ - addPythonProject(projects: PythonProject | PythonProject[]): void; + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { /** - * Remove a python project from the list of projects. + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. * - * @param project The project to remove. + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. */ - removePythonProject(project: PythonProject): void; + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; /** - * Get all python projects. + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. */ - getPythonProjects(): readonly PythonProject[]; + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { /** - * Event raised when python projects are added or removed. - * @see {@link DidChangePythonProjectsEventArgs} + * Name of the task to run. */ - onDidChangePythonProjects: Event; + name: string; /** - * Get the python project for a given URI. + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. * - * @param uri The URI of the project - * @returns The project or `undefined` if not found. */ - getPythonProject(uri: Uri): PythonProject | undefined; + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; /** - * Register a Python project creator. + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeType: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. * - * @param creator The project creator to register. - * @returns A disposable that can be used to unregister the project creator. - * @see {@link PythonProjectCreator} + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required.If not provided, + * it fetches the environment variables for the global scope. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. */ - registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; + getEnvironmentVariables( + uri: Uri | undefined, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; } /** * The API for interacting with Python environments, package managers, and projects. */ -export interface PythonEnvironmentApi extends PythonEnvironmentManagerApi, PythonPackageManagerApi, PythonProjectApi {} +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} diff --git a/src/common/childProcess.apis.ts b/src/common/childProcess.apis.ts new file mode 100644 index 0000000..8ae2f26 --- /dev/null +++ b/src/common/childProcess.apis.ts @@ -0,0 +1,28 @@ +import * as cp from 'child_process'; + +/** + * Spawns a new process using the specified command and arguments. + * This function abstracts cp.spawn to make it easier to mock in tests. + * + * When stdio: 'pipe' is used, returns ChildProcessWithoutNullStreams. + * Otherwise returns the standard ChildProcess. + */ + +// Overload for stdio: 'pipe' - guarantees non-null streams +export function spawnProcess( + command: string, + args: string[], + options: cp.SpawnOptions & { stdio: 'pipe' }, +): cp.ChildProcessWithoutNullStreams; + +// Overload for general case +export function spawnProcess(command: string, args: string[], options?: cp.SpawnOptions): cp.ChildProcess; + +// Implementation - delegates to cp.spawn to preserve its typing magic +export function spawnProcess( + command: string, + args: string[], + options?: cp.SpawnOptions, +): cp.ChildProcess | cp.ChildProcessWithoutNullStreams { + return cp.spawn(command, args, options ?? {}); +} diff --git a/src/common/command.api.ts b/src/common/command.api.ts new file mode 100644 index 0000000..4d1e2b5 --- /dev/null +++ b/src/common/command.api.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { commands } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; + +export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); +} + +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} diff --git a/src/common/commands.ts b/src/common/commands.ts new file mode 100644 index 0000000..2b9bd98 --- /dev/null +++ b/src/common/commands.ts @@ -0,0 +1,3 @@ +export namespace Commands { + export const viewLogs = 'python-envs.viewLogs'; +} diff --git a/src/common/constants.ts b/src/common/constants.ts index 2af994e..08612bf 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -2,6 +2,7 @@ import * as path from 'path'; export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; export const PYTHON_EXTENSION_ID = 'ms-python.python'; +export const JUPYTER_EXTENSION_ID = 'ms-toolsai.jupyter'; export const EXTENSION_ROOT_DIR = path.dirname(__dirname); export const DEFAULT_PACKAGE_MANAGER_ID = 'ms-python.python:pip'; @@ -25,3 +26,6 @@ export const KNOWN_FILES = [ ]; export const KNOWN_TEMPLATE_ENDINGS = ['.j2', '.jinja2']; + +export const NEW_PROJECT_TEMPLATES_FOLDER = path.join(EXTENSION_ROOT_DIR, 'files', 'templates'); +export const NotebookCellScheme = 'vscode-notebook-cell'; diff --git a/src/common/env.apis.ts b/src/common/env.apis.ts index 157c9a4..21369ed 100644 --- a/src/common/env.apis.ts +++ b/src/common/env.apis.ts @@ -3,3 +3,7 @@ import { env, Uri } from 'vscode'; export function launchBrowser(uri: string | Uri): Thenable { return env.openExternal(uri instanceof Uri ? uri : Uri.parse(uri)); } + +export function clipboardWriteText(text: string): Thenable { + return env.clipboard.writeText(text); +} diff --git a/src/common/errors/utils.ts b/src/common/errors/utils.ts index d917f74..023467b 100644 --- a/src/common/errors/utils.ts +++ b/src/common/errors/utils.ts @@ -1,5 +1,7 @@ import * as stackTrace from 'stack-trace'; -import { commands, LogOutputChannel, window } from 'vscode'; +import { commands, LogOutputChannel } from 'vscode'; +import { Common } from '../localize'; +import { showErrorMessage, showWarningMessage } from '../window.apis'; export function parseStack(ex: Error) { if (ex.stack && Array.isArray(ex.stack)) { @@ -9,9 +11,20 @@ export function parseStack(ex: Error) { return stackTrace.parse.call(stackTrace, ex); } -export async function showErrorMessage(message: string, log?: LogOutputChannel) { - const result = await window.showErrorMessage(message, 'View Logs'); - if (result === 'View Logs') { +export async function showErrorMessageWithLogs(message: string, log?: LogOutputChannel) { + const result = await showErrorMessage(message, Common.viewLogs); + if (result === Common.viewLogs) { + if (log) { + log.show(); + } else { + commands.executeCommand('python-envs.viewLogs'); + } + } +} + +export async function showWarningMessageWithLogs(message: string, log?: LogOutputChannel) { + const result = await showWarningMessage(message, Common.viewLogs); + if (result === Common.viewLogs) { if (log) { log.show(); } else { diff --git a/src/common/extVersion.ts b/src/common/extVersion.ts new file mode 100644 index 0000000..39229c0 --- /dev/null +++ b/src/common/extVersion.ts @@ -0,0 +1,20 @@ +import { PYTHON_EXTENSION_ID } from './constants'; +import { getExtension } from './extension.apis'; +import { traceError } from './logging'; + +export function ensureCorrectVersion() { + const extension = getExtension(PYTHON_EXTENSION_ID); + if (!extension) { + return; + } + + const version = extension.packageJSON.version; + const parts = version.split('.'); + const major = parseInt(parts[0]); + const minor = parseInt(parts[1]); + if (major >= 2025 || (major === 2024 && minor >= 23)) { + return; + } + traceError('Incompatible Python extension. Please update `ms-python.python` to version 2024.23 or later.'); + throw new Error('Incompatible Python extension. Please update `ms-python.python` to version 2024.23 or later.'); +} diff --git a/src/common/extension.apis.ts b/src/common/extension.apis.ts index 23ce30e..c77594b 100644 --- a/src/common/extension.apis.ts +++ b/src/common/extension.apis.ts @@ -1,5 +1,10 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { Extension, extensions } from 'vscode'; export function getExtension(extensionId: string): Extension | undefined { return extensions.getExtension(extensionId); } + +export function allExtensions(): readonly Extension[] { + return extensions.all; +} diff --git a/src/common/extensions.apis.ts b/src/common/extensions.apis.ts deleted file mode 100644 index 4340546..0000000 --- a/src/common/extensions.apis.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { extensions } from 'vscode'; -import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from './constants'; -import { parseStack } from './errors/utils'; - -export function getCallingExtension(): string { - const frames = parseStack(new Error()); - - const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID]; - const otherExts = extensions.all.map((ext) => ext.id).filter((id) => !pythonExts.includes(id)); - - for (const frame of frames) { - for (const ext of otherExts) { - const filename = frame.getFileName(); - if (filename) { - const parts = filename.split(/\\\//); - if (parts.includes(ext)) { - return ext; - } - } - } - } - - return PYTHON_EXTENSION_ID; -} diff --git a/src/common/lm.apis.ts b/src/common/lm.apis.ts new file mode 100644 index 0000000..61de4fa --- /dev/null +++ b/src/common/lm.apis.ts @@ -0,0 +1,4 @@ +import * as vscode from 'vscode'; +export function registerTools(name: string, tool: vscode.LanguageModelTool): vscode.Disposable { + return vscode.lm.registerTool(name, tool); +} diff --git a/src/common/localize.ts b/src/common/localize.ts index c6fb817..1674a39 100644 --- a/src/common/localize.ts +++ b/src/common/localize.ts @@ -1,22 +1,195 @@ import { l10n } from 'vscode'; +import { Commands } from './commands'; export namespace Common { - export const recommended = l10n.t('recommended'); + export const recommended = l10n.t('Recommended'); export const install = l10n.t('Install'); export const uninstall = l10n.t('Uninstall'); export const openInBrowser = l10n.t('Open in Browser'); export const openInEditor = l10n.t('Open in Editor'); + export const browse = l10n.t('Browse'); + export const selectFolder = l10n.t('Select Folder'); + export const viewLogs = l10n.t('View Logs'); + export const yes = l10n.t('Yes'); + export const no = l10n.t('No'); + export const ok = l10n.t('Ok'); + export const quickCreate = l10n.t('Quick Create'); + export const installPython = l10n.t('Install Python'); +} + +export namespace WorkbenchStrings { + export const installExtension = l10n.t('Install Extension'); } export namespace Interpreter { export const statusBarSelect = l10n.t('Select Interpreter'); + export const browsePath = l10n.t('Browse...'); + export const createVirtualEnvironment = l10n.t('Create Virtual Environment...'); } export namespace PackageManagement { + export const install = l10n.t('Install'); + export const uninstall = l10n.t('Uninstall'); + export const installed = l10n.t('Installed'); + export const commonPackages = l10n.t('Common Packages'); export const selectPackagesToInstall = l10n.t('Select packages to install'); export const enterPackageNames = l10n.t('Enter package names'); - export const commonPackages = l10n.t('Common packages'); - export const workspacePackages = l10n.t('Workspace packages'); + export const searchCommonPackages = l10n.t('Search `PyPI` packages'); + export const searchCommonPackagesDescription = l10n.t('Search and install popular `PyPI` packages'); + export const workspaceDependencies = l10n.t('Install project dependencies'); + export const workspaceDependenciesDescription = l10n.t('Install packages found in dependency files.'); export const selectPackagesToUninstall = l10n.t('Select packages to uninstall'); export const enterPackagesPlaceHolder = l10n.t('Enter package names separated by space'); + export const editArguments = l10n.t('Edit arguments'); + export const skipPackageInstallation = l10n.t('Skip package installation'); +} + +export namespace Pickers { + export namespace Environments { + export const selectExecutable = l10n.t('Select Python Executable'); + export const selectEnvironment = l10n.t('Select a Python Environment'); + } + + export namespace Packages { + export const selectOption = l10n.t('Select an option'); + } + + export namespace Managers { + export const selectEnvironmentManager = l10n.t('Select an environment manager'); + export const selectPackageManager = l10n.t('Select a package manager'); + export const selectProjectCreator = l10n.t('Select a project creator'); + } + + export namespace Project { + export const selectProject = l10n.t('Select a project, folder or script'); + export const selectProjects = l10n.t('Select one or more projects, folders or scripts'); + } +} + +export namespace ProjectViews { + export const noPackageManager = l10n.t('No package manager found'); + export const waitingForEnvManager = l10n.t('Waiting for environment managers to load'); + export const noEnvironmentManager = l10n.t('Environment manager not found'); + export const noEnvironmentManagerDescription = l10n.t( + 'Install an environment manager to get started. If you have installed then it might be loading or errored', + ); + export const noEnvironmentProvided = l10n.t('No environment provided by:'); + export const noPackages = l10n.t('No packages found'); +} + +export namespace VenvManagerStrings { + export const venvManagerDescription = l10n.t('Manages virtual environments created using `venv`'); + export const venvInitialize = l10n.t('Initializing virtual environments'); + export const venvRefreshing = l10n.t('Refreshing virtual environments'); + export const venvGlobalFolder = l10n.t('Select a folder to create a global virtual environment'); + export const venvGlobalFoldersSetting = l10n.t('Venv Folders Setting'); + + export const venvErrorNoBasePython = l10n.t('No base Python found'); + export const venvErrorNoPython3 = l10n.t('Did not find any base Python 3'); + + export const venvName = l10n.t('Enter a name for the virtual environment'); + export const venvNameErrorEmpty = l10n.t('Name cannot be empty'); + export const venvNameErrorExists = l10n.t('A folder with the same name already exists'); + export const venvCreateFailed = l10n.t('Failed to create virtual environment'); + + export const venvRemoving = l10n.t('Removing virtual environment'); + export const venvRemoveFailed = l10n.t('Failed to remove virtual environment'); + + export const installEditable = l10n.t('Install project as editable'); + export const searchingDependencies = l10n.t('Searching for dependencies'); + + export const selectQuickOrCustomize = l10n.t('Select environment creation mode'); + export const quickCreate = l10n.t('Quick Create'); + export const quickCreateDescription = l10n.t('Create a virtual environment in the workspace root'); + export const customize = l10n.t('Custom'); + export const customizeDescription = l10n.t('Choose python version, location, packages, name, etc.'); +} + +export namespace SysManagerStrings { + export const sysManagerDescription = l10n.t('Manages Global Python installs'); + export const sysManagerRefreshing = l10n.t('Refreshing Global Python interpreters'); + export const sysManagerDiscovering = l10n.t('Discovering Global Python interpreters'); + + export const selectInstall = l10n.t('Select packages to install'); + export const selectUninstall = l10n.t('Select packages to uninstall'); + + export const packageRefreshError = l10n.t('Error refreshing packages'); +} + +export namespace CondaStrings { + export const condaManager = l10n.t('Manages Conda environments'); + export const condaDiscovering = l10n.t('Discovering Conda environments'); + export const condaRefreshingEnvs = l10n.t('Refreshing Conda environments'); + + export const condaPackageMgr = l10n.t('Manages Conda packages'); + export const condaRefreshingPackages = l10n.t('Refreshing Conda packages'); + export const condaInstallingPackages = l10n.t('Installing Conda packages'); + export const condaInstallError = l10n.t('Error installing Conda packages'); + export const condaUninstallingPackages = l10n.t('Uninstalling Conda packages'); + export const condaUninstallError = l10n.t('Error uninstalling Conda packages'); + + export const condaNamed = l10n.t('Named'); + export const condaPrefix = l10n.t('Prefix'); + + export const condaNamedDescription = l10n.t('Create a named conda environment'); + export const condaPrefixDescription = l10n.t('Create environment in your workspace'); + export const condaSelectEnvType = l10n.t('Select the type of conda environment to create'); + + export const condaNamedInput = l10n.t('Enter the name of the conda environment to create'); + + export const condaCreateFailed = l10n.t('Failed to create conda environment'); + export const condaRemoveFailed = l10n.t('Failed to remove conda environment'); + export const condaExists = l10n.t('Environment already exists'); + + export const quickCreateCondaNoEnvRoot = l10n.t('No conda environment root found'); + export const quickCreateCondaNoName = l10n.t('Could not generate a name for env'); + + export const condaMissingPython = l10n.t('No Python found in the selected conda environment'); + export const condaMissingPythonNoFix = l10n.t( + 'No Python found in the selected conda environment. Please select another environment or install Python manually.', + ); +} + +export namespace PyenvStrings { + export const pyenvManager = l10n.t('Manages Pyenv Python versions'); + export const pyenvDiscovering = l10n.t('Discovering Pyenv Python versions'); + export const pyenvRefreshing = l10n.t('Refreshing Pyenv Python versions'); +} + +export namespace PipenvStrings { + export const pipenvManager = l10n.t('Manages Pipenv environments'); + export const pipenvDiscovering = l10n.t('Discovering Pipenv environments'); + export const pipenvRefreshing = l10n.t('Refreshing Pipenv environments'); +} + +export namespace PoetryStrings { + export const poetryManager = l10n.t('Manages Poetry environments'); + export const poetryDiscovering = l10n.t('Discovering Poetry environments'); + export const poetryRefreshing = l10n.t('Refreshing Poetry environments'); +} + +export namespace ProjectCreatorString { + export const addExistingProjects = l10n.t('Add Existing Projects'); + export const autoFindProjects = l10n.t('Auto Find Projects'); + export const selectProjects = l10n.t('Select Python projects'); + export const selectFilesOrFolders = l10n.t('Select Project folders or Python files'); + export const autoFindProjectsDescription = l10n.t( + 'Automatically find folders with `pyproject.toml` or `setup.py` files.', + ); + + export const noProjectsFound = l10n.t('No projects found'); +} + +export namespace EnvViewStrings { + export const selectedGlobalTooltip = l10n.t('This environment is selected for non-workspace files'); + export const selectedWorkspaceTooltip = l10n.t('This environment is selected for project files'); +} + +export namespace ActivationStrings { + export const envCollectionDescription = l10n.t('Environment variables for shell activation'); + export const revertedShellStartupScripts = l10n.t( + 'Removed shell startup profile code for Python environment activation. See [logs](command:{0})', + Commands.viewLogs, + ); + export const activatingEnvironment = l10n.t('Activating environment'); } diff --git a/src/common/persistentState.ts b/src/common/persistentState.ts index 4f81b39..c6b0f26 100644 --- a/src/common/persistentState.ts +++ b/src/common/persistentState.ts @@ -1,6 +1,6 @@ import { ExtensionContext, Memento } from 'vscode'; -import { createDeferred, Deferred } from './utils/deferred'; import { traceError } from './logging'; +import { createDeferred, Deferred } from './utils/deferred'; export interface PersistentState { get(key: string, defaultValue?: T): Promise; @@ -9,7 +9,6 @@ export interface PersistentState { } class PersistentStateImpl implements PersistentState { - private keys: string[] = []; private clearing: Deferred; constructor(private readonly momento: Memento) { this.clearing = createDeferred(); @@ -24,10 +23,6 @@ class PersistentStateImpl implements PersistentState { } async set(key: string, value: T): Promise { await this.clearing.promise; - - if (!this.keys.includes(key)) { - this.keys.push(key); - } await this.momento.update(key, value); const before = JSON.stringify(value); @@ -40,9 +35,8 @@ class PersistentStateImpl implements PersistentState { async clear(keys?: string[]): Promise { if (this.clearing.completed) { this.clearing = createDeferred(); - const _keys = keys ?? this.keys; + const _keys = keys ?? this.momento.keys(); await Promise.all(_keys.map((key) => this.momento.update(key, undefined))); - this.keys = this.keys.filter((k) => _keys.includes(k)); this.clearing.resolve(); } return this.clearing.promise; @@ -64,3 +58,9 @@ export function getWorkspacePersistentState(): Promise { export function getGlobalPersistentState(): Promise { return _global.promise; } + +export async function clearPersistentState(): Promise { + const [workspace, global] = await Promise.all([_workspace.promise, _global.promise]); + await Promise.all([workspace.clear(), global.clear()]); + return undefined; +} diff --git a/src/common/pickers.ts b/src/common/pickers.ts deleted file mode 100644 index b305322..0000000 --- a/src/common/pickers.ts +++ /dev/null @@ -1,467 +0,0 @@ -import { QuickPickItem, QuickPickItemButtonEvent, QuickPickItemKind, ThemeIcon, Uri, window } from 'vscode'; -import { - GetEnvironmentsScope, - IconPath, - Installable, - Package, - PythonEnvironment, - PythonProject, - PythonProjectCreator, -} from '../api'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { InternalEnvironmentManager, InternalPackageManager } from '../internal.api'; -import { Common, PackageManagement } from './localize'; -import { EXTENSION_ROOT_DIR } from './constants'; -import { showQuickPickWithButtons, showTextDocument } from './window.apis'; -import { launchBrowser } from './env.apis'; -import { traceWarn } from './logging'; - -export async function pickProject(pws: ReadonlyArray): Promise { - if (pws.length > 1) { - const items = pws.map((pw) => ({ - label: path.basename(pw.uri.fsPath), - description: pw.uri.fsPath, - pw: pw, - })); - const item = await window.showQuickPick(items, { - placeHolder: 'Select a project, folder or script', - ignoreFocusOut: true, - }); - if (item) { - return item.pw; - } - } else if (pws.length === 1) { - return pws[0]; - } - return undefined; -} - -export async function pickEnvironmentManager( - managers: InternalEnvironmentManager[], - defaultMgr?: InternalEnvironmentManager, -): Promise { - const items = managers.map((m) => ({ - label: defaultMgr?.id === m.id ? `${m.displayName} (${Common.recommended})` : m.displayName, - description: m.description, - id: m.id, - })); - const item = await window.showQuickPick(items, { - placeHolder: 'Select an environment manager', - ignoreFocusOut: true, - }); - return item?.id; -} - -export async function pickPackageManager( - managers: InternalPackageManager[], - defaultMgr?: InternalPackageManager, -): Promise { - const items = managers.map((m) => ({ - label: defaultMgr?.id === m.id ? `${m.displayName} (${Common.recommended})` : m.displayName, - description: m.description, - id: m.id, - })); - - const item = await window.showQuickPick(items, { - placeHolder: 'Select a package manager', - ignoreFocusOut: true, - }); - return item?.id; -} - -type QuickPickIcon = - | Uri - | { - /** - * The icon path for the light theme. - */ - light: Uri; - /** - * The icon path for the dark theme. - */ - dark: Uri; - } - | ThemeIcon - | undefined; - -function getIconPath(i: IconPath | undefined): QuickPickIcon { - if (i === undefined || i instanceof Uri || i instanceof ThemeIcon) { - return i; - } - - if (typeof i === 'string') { - return Uri.file(i); - } - - return { - light: i.light instanceof Uri ? i.light : Uri.file(i.light), - dark: i.dark instanceof Uri ? i.dark : Uri.file(i.dark), - }; -} - -export interface SelectionResult { - selected: PythonEnvironment; - manager: InternalEnvironmentManager; -} - -export async function pickEnvironment( - managers: InternalEnvironmentManager[], - scope: GetEnvironmentsScope, - recommended?: SelectionResult, -): Promise { - const items: (QuickPickItem | (QuickPickItem & { e: SelectionResult }))[] = []; - - if (recommended) { - items.push( - { - label: Common.recommended, - kind: QuickPickItemKind.Separator, - }, - { - label: recommended.selected.displayName, - description: recommended.selected.description, - e: recommended, - iconPath: getIconPath(recommended.selected.iconPath), - }, - ); - } - - for (const manager of managers) { - items.push({ - label: manager.displayName, - kind: QuickPickItemKind.Separator, - }); - const envs = await manager.getEnvironments(scope); - items.push( - ...envs.map((e) => { - return { - label: e.displayName ?? e.name, - description: e.description, - e: { selected: e, manager: manager }, - iconPath: getIconPath(e.iconPath), - }; - }), - ); - } - const selected = await window.showQuickPick(items, { - placeHolder: `Select a Python Environment`, - ignoreFocusOut: true, - }); - return (selected as { e: SelectionResult })?.e; -} - -export async function pickEnvironmentFrom(environments: PythonEnvironment[]): Promise { - const items = environments.map((e) => ({ - label: e.displayName ?? e.name, - description: e.description, - e: e, - iconPath: getIconPath(e.iconPath), - })); - const selected = await window.showQuickPick(items, { - placeHolder: 'Select Python Environment', - ignoreFocusOut: true, - }); - return (selected as { e: PythonEnvironment })?.e; -} - -export async function pickCreator(creators: PythonProjectCreator[]): Promise { - if (creators.length === 0) { - return; - } - - if (creators.length === 1) { - return creators[0]; - } - - const items: (QuickPickItem & { c: PythonProjectCreator })[] = creators.map((c) => ({ - label: c.displayName ?? c.name, - description: c.description, - c: c, - })); - const selected = await window.showQuickPick(items, { - placeHolder: 'Select a project creator', - ignoreFocusOut: true, - }); - return (selected as { c: PythonProjectCreator })?.c; -} - -export async function pickPackageOptions(): Promise { - const items = [ - { - label: Common.install, - description: 'Install packages', - }, - { - label: Common.uninstall, - description: 'Uninstall packages', - }, - ]; - const selected = await window.showQuickPick(items, { - placeHolder: 'Select an option', - ignoreFocusOut: true, - }); - return selected?.label; -} - -export async function enterPackageManually(filler?: string): Promise { - const input = await window.showInputBox({ - placeHolder: PackageManagement.enterPackagesPlaceHolder, - value: filler, - ignoreFocusOut: true, - }); - return input?.split(' '); -} - -async function getCommonPackages(): Promise { - const pipData = path.join(EXTENSION_ROOT_DIR, 'files', 'common_packages.txt'); - const data = await fs.readFile(pipData, { encoding: 'utf-8' }); - const packages = data.split(/\r?\n/).filter((l) => l.trim().length > 0); - - return packages.map((p) => { - return { - displayName: p, - args: [p], - uri: Uri.parse(`https://pypi.org/project/${p}`), - }; - }); -} - -export const OPEN_BROWSER_BUTTON = { - iconPath: new ThemeIcon('globe'), - tooltip: Common.openInBrowser, -}; - -export const OPEN_EDITOR_BUTTON = { - iconPath: new ThemeIcon('go-to-file'), - tooltip: Common.openInBrowser, -}; - -function handleButton(uri?: Uri) { - if (uri) { - if (uri.scheme.toLowerCase().startsWith('http')) { - launchBrowser(uri); - } else { - showTextDocument(uri); - } - } -} - -interface PackageQuickPickItem extends QuickPickItem { - uri?: Uri; - args?: string[]; -} - -function getDetail(i: Installable): string | undefined { - if (i.args && i.args.length > 0) { - if (i.args.length === 1 && i.args[0] === i.displayName) { - return undefined; - } - return i.args.join(' '); - } - return undefined; -} - -function installableToQuickPickItem(i: Installable): PackageQuickPickItem { - const detail = i.description ? getDetail(i) : undefined; - const description = i.description ? i.description : getDetail(i); - const buttons = i.uri - ? i.uri.scheme.startsWith('http') - ? [OPEN_BROWSER_BUTTON] - : [OPEN_EDITOR_BUTTON] - : undefined; - return { - label: i.displayName, - detail, - description, - buttons, - uri: i.uri, - args: i.args, - }; -} - -async function getPackageType(packageManager: InternalPackageManager): Promise { - if (!packageManager.supportsGetInstallable) { - return PackageManagement.commonPackages; - } - - const items: QuickPickItem[] = [ - { - label: PackageManagement.enterPackageNames, - alwaysShow: true, - }, - { - label: PackageManagement.workspacePackages, - alwaysShow: true, - }, - { - label: PackageManagement.commonPackages, - alwaysShow: true, - }, - ]; - const selected = await window.showQuickPick(items, { - placeHolder: PackageManagement.selectPackagesToInstall, - ignoreFocusOut: true, - }); - - return selected?.label; -} - -function getGroupedItems(items: Installable[]): PackageQuickPickItem[] { - const groups = new Map(); - const workspaceInstallable: Installable[] = []; - - items.forEach((i) => { - if (i.group) { - let group = groups.get(i.group); - if (!group) { - group = []; - groups.set(i.group, group); - } - group.push(i); - } else { - workspaceInstallable.push(i); - } - }); - - const result: PackageQuickPickItem[] = []; - groups.forEach((group, key) => { - result.push({ - label: key, - kind: QuickPickItemKind.Separator, - }); - result.push(...group.map(installableToQuickPickItem)); - }); - - if (workspaceInstallable.length > 0) { - result.push({ - label: PackageManagement.workspacePackages, - kind: QuickPickItemKind.Separator, - }); - result.push(...workspaceInstallable.map(installableToQuickPickItem)); - } - - return result; -} - -async function getWorkspacePackages( - packageManager: InternalPackageManager, - environment: PythonEnvironment, -): Promise { - const items: PackageQuickPickItem[] = [ - { - label: PackageManagement.enterPackageNames, - alwaysShow: true, - }, - ]; - - let installable = await packageManager?.getInstallable(environment); - if (installable && installable.length > 0) { - items.push(...getGroupedItems(installable)); - } else { - traceWarn(`No installable packages found for ${packageManager.id}: ${environment.environmentPath.fsPath}`); - installable = await getCommonPackages(); - items.push( - { - label: PackageManagement.commonPackages, - kind: QuickPickItemKind.Separator, - }, - ...installable.map(installableToQuickPickItem), - ); - } - - const selected = await showQuickPickWithButtons( - items, - { - placeHolder: PackageManagement.selectPackagesToInstall, - ignoreFocusOut: true, - canPickMany: true, - }, - undefined, - async (e: QuickPickItemButtonEvent) => { - handleButton(e.item.uri); - }, - ); - - if (selected && Array.isArray(selected)) { - if (selected.find((s) => s.label === PackageManagement.enterPackageNames)) { - const filler = selected - .filter((s) => s.label !== PackageManagement.enterPackageNames) - .flatMap((s) => s.args ?? []) - .join(' '); - return enterPackageManually(filler); - } else { - return selected.flatMap((s) => s.args ?? []); - } - } -} - -export async function getPackagesToInstall( - packageManager: InternalPackageManager, - environment: PythonEnvironment, -): Promise { - const packageType = await getPackageType(packageManager); - - if (!packageType) { - return; - } - - if (packageType === PackageManagement.enterPackageNames) { - return enterPackageManually(); - } else if (packageType === PackageManagement.workspacePackages) { - return getWorkspacePackages(packageManager, environment); - } - - const common = await getCommonPackages(); - const items: PackageQuickPickItem[] = [ - { - label: PackageManagement.enterPackageNames, - alwaysShow: true, - }, - { - label: PackageManagement.commonPackages, - kind: QuickPickItemKind.Separator, - }, - ]; - - items.push(...common.map(installableToQuickPickItem)); - - const selected = await showQuickPickWithButtons( - items, - { - placeHolder: PackageManagement.selectPackagesToInstall, - ignoreFocusOut: true, - canPickMany: true, - }, - undefined, - async (e: QuickPickItemButtonEvent) => { - handleButton(e.item.uri); - }, - ); - - if (selected && Array.isArray(selected)) { - if (selected.find((s) => s.label === PackageManagement.enterPackageNames)) { - const filler = selected - .filter((s) => s.label !== PackageManagement.enterPackageNames) - .map((s) => s.label) - .join(' '); - return enterPackageManually(filler); - } else { - return selected.map((s) => s.label); - } - } -} - -export async function getPackagesToUninstall(packages: Package[]): Promise { - const items = packages.map((p) => ({ - label: p.name, - description: p.version, - p: p, - })); - const selected = await window.showQuickPick(items, { - placeHolder: PackageManagement.selectPackagesToUninstall, - ignoreFocusOut: true, - canPickMany: true, - }); - return selected?.map((s) => s.p); -} diff --git a/src/common/pickers/environments.ts b/src/common/pickers/environments.ts new file mode 100644 index 0000000..5f6ab63 --- /dev/null +++ b/src/common/pickers/environments.ts @@ -0,0 +1,224 @@ +import { ProgressLocation, QuickInputButtons, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode'; +import { CreateEnvironmentOptions, IconPath, PythonEnvironment, PythonProject } from '../../api'; +import { InternalEnvironmentManager } from '../../internal.api'; +import { Common, Interpreter, Pickers } from '../localize'; +import { traceError } from '../logging'; +import { EventNames } from '../telemetry/constants'; +import { sendTelemetryEvent } from '../telemetry/sender'; +import { isWindows } from '../utils/platformUtils'; +import { handlePythonPath } from '../utils/pythonPath'; +import { showOpenDialog, showQuickPick, showQuickPickWithButtons, withProgress } from '../window.apis'; +import { pickEnvironmentManager } from './managers'; + +type QuickPickIcon = + | Uri + | { + light: Uri; + dark: Uri; + } + | ThemeIcon + | undefined; + +function getIconPath(i: IconPath | undefined): QuickPickIcon { + if (i === undefined || i instanceof ThemeIcon || i instanceof Uri) { + return i; + } + + if (typeof i === 'string') { + return Uri.file(i); + } + + return { + light: i.light instanceof Uri ? i.light : Uri.file(i.light), + dark: i.dark instanceof Uri ? i.dark : Uri.file(i.dark), + }; +} + +interface EnvironmentPickOptions { + recommended?: PythonEnvironment; + showBackButton?: boolean; + projects: PythonProject[]; +} +async function browseForPython( + managers: InternalEnvironmentManager[], + projectEnvManagers: InternalEnvironmentManager[], +): Promise { + const filters = isWindows() ? { python: ['exe'] } : undefined; + const uris = await showOpenDialog({ + canSelectFiles: true, + canSelectFolders: false, + canSelectMany: false, + filters, + title: Pickers.Environments.selectExecutable, + }); + if (!uris || uris.length === 0) { + return; + } + const uri = uris[0]; + + const environment = await withProgress( + { + location: ProgressLocation.Notification, + cancellable: false, + }, + async (reporter, token) => { + const env = await handlePythonPath(uri, managers, projectEnvManagers, reporter, token); + return env; + }, + ); + return environment; +} + +async function createEnvironment( + managers: InternalEnvironmentManager[], + projectEnvManagers: InternalEnvironmentManager[], + options: EnvironmentPickOptions, +): Promise { + const managerId = await pickEnvironmentManager( + managers.filter((m) => m.supportsCreate), + projectEnvManagers.filter((m) => m.supportsCreate), + ); + + let manager: InternalEnvironmentManager | undefined; + let createOptions: CreateEnvironmentOptions | undefined = undefined; + if (managerId?.includes(`QuickCreate#`)) { + manager = managers.find((m) => m.id === managerId.split('#')[1]); + createOptions = { + projects: projectEnvManagers.map((m) => m), + quickCreate: true, + } as CreateEnvironmentOptions; + } else { + manager = managers.find((m) => m.id === managerId); + } + + if (manager) { + try { + // add telemetry here + const env = await manager.create( + options.projects.map((p) => p.uri), + createOptions, + ); + return env; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + return createEnvironment(managers, projectEnvManagers, options); + } + traceError(`Failed to create environment using ${manager.id}`, ex); + throw ex; + } + } +} + +async function pickEnvironmentImpl( + items: (QuickPickItem | (QuickPickItem & { result: PythonEnvironment }))[], + managers: InternalEnvironmentManager[], + projectEnvManagers: InternalEnvironmentManager[], + options: EnvironmentPickOptions, +): Promise { + const selected = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Environments.selectEnvironment, + ignoreFocusOut: true, + showBackButton: options?.showBackButton, + }); + + if (selected && !Array.isArray(selected)) { + if (selected.label === Interpreter.browsePath) { + return browseForPython(managers, projectEnvManagers); + } else if (selected.label === Interpreter.createVirtualEnvironment) { + sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, { + manager: 'none', + triggeredLocation: 'pickEnv', + }); + return createEnvironment(managers, projectEnvManagers, options); + } + return (selected as { result: PythonEnvironment })?.result; + } + return undefined; +} + +export async function pickEnvironment( + managers: InternalEnvironmentManager[], + projectEnvManagers: InternalEnvironmentManager[], + options: EnvironmentPickOptions, +): Promise { + const items: (QuickPickItem | (QuickPickItem & { result: PythonEnvironment }))[] = [ + { + label: Interpreter.browsePath, + iconPath: new ThemeIcon('folder'), + }, + { + label: '', + kind: QuickPickItemKind.Separator, + }, + { + label: Interpreter.createVirtualEnvironment, + iconPath: new ThemeIcon('add'), + }, + ]; + + if (options?.recommended) { + const pathDescription = options.recommended.displayPath; + const description = + options.recommended.description && options.recommended.description.trim() + ? `${options.recommended.description} (${pathDescription})` + : pathDescription; + + items.push( + { + label: Common.recommended, + kind: QuickPickItemKind.Separator, + }, + { + label: options.recommended.displayName, + description: description, + result: options.recommended, + iconPath: getIconPath(options.recommended.iconPath), + }, + ); + } + + for (const manager of managers) { + items.push({ + label: manager.displayName, + kind: QuickPickItemKind.Separator, + }); + const envs = await manager.getEnvironments('all'); + items.push( + ...envs.map((e) => { + const pathDescription = e.displayPath; + const description = + e.description && e.description.trim() ? `${e.description} (${pathDescription})` : pathDescription; + + return { + label: e.displayName ?? e.name, + description: description, + result: e, + manager: manager, + iconPath: getIconPath(e.iconPath), + }; + }), + ); + } + + return pickEnvironmentImpl(items, managers, projectEnvManagers, options); +} + +export async function pickEnvironmentFrom(environments: PythonEnvironment[]): Promise { + const items = environments.map((e) => { + const pathDescription = e.displayPath; + const description = + e.description && e.description.trim() ? `${e.description} (${pathDescription})` : pathDescription; + + return { + label: e.displayName ?? e.name, + description: description, + e: e, + iconPath: getIconPath(e.iconPath), + }; + }); + const selected = await showQuickPick(items, { + placeHolder: Pickers.Environments.selectEnvironment, + ignoreFocusOut: true, + }); + return (selected as { e: PythonEnvironment })?.e; +} diff --git a/src/common/pickers/managers.ts b/src/common/pickers/managers.ts new file mode 100644 index 0000000..ad37e1e --- /dev/null +++ b/src/common/pickers/managers.ts @@ -0,0 +1,247 @@ +import { commands, QuickInputButtons, QuickPickItem, QuickPickItemKind, workspace, WorkspaceFolder } from 'vscode'; +import { PythonProjectCreator } from '../../api'; +import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api'; +import { Common, Pickers } from '../localize'; +import { showQuickPickWithButtons } from '../window.apis'; + +function getDescription(mgr: InternalEnvironmentManager | InternalPackageManager): string | undefined { + if (mgr.description) { + return mgr.description; + } + if (mgr.tooltip) { + const tooltip = mgr.tooltip; + if (typeof tooltip === 'string') { + return tooltip; + } + return tooltip.value; + } + return undefined; +} + +export async function pickEnvironmentManager( + managers: InternalEnvironmentManager[], + defaultManagers?: InternalEnvironmentManager[], + showBackButton?: boolean, +): Promise { + if (managers.length === 0) { + return; + } + + if (managers.length === 1 && !managers[0].supportsQuickCreate) { + // If there's only one manager and it doesn't support quick create, return its ID directly. + return managers[0].id; + } + + const items: (QuickPickItem | (QuickPickItem & { id: string }))[] = []; + if (defaultManagers && defaultManagers.length > 0) { + items.push({ + label: Common.recommended, + kind: QuickPickItemKind.Separator, + }); + if (defaultManagers.length === 1 && defaultManagers[0].supportsQuickCreate) { + const defaultMgr = defaultManagers[0]; + const details = defaultMgr.quickCreateConfig(); + if (details) { + items.push({ + label: Common.quickCreate, + description: `${defaultMgr.displayName} • ${details.description}`, + detail: details.detail, + id: `QuickCreate#${defaultMgr.id}`, + }); + } + } + items.push( + ...defaultManagers.map((defaultMgr) => ({ + label: defaultMgr.displayName, + description: getDescription(defaultMgr), + id: defaultMgr.id, + })), + { + label: '', + kind: QuickPickItemKind.Separator, + }, + ); + } + items.push( + ...managers + .filter((m) => !defaultManagers?.includes(m)) + .map((m) => ({ + label: m.displayName, + description: getDescription(m), + id: m.id, + })), + ); + const item = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Managers.selectEnvironmentManager, + ignoreFocusOut: true, + showBackButton, + }); + return (item as QuickPickItem & { id: string })?.id; +} + +export async function pickPackageManager( + managers: InternalPackageManager[], + defaultManagers?: InternalPackageManager[], +): Promise { + if (managers.length === 0) { + return; + } + + if (managers.length === 1) { + return managers[0].id; + } + + const items: (QuickPickItem | (QuickPickItem & { id: string }))[] = []; + if (defaultManagers && defaultManagers.length > 0) { + items.push( + { + label: Common.recommended, + kind: QuickPickItemKind.Separator, + }, + ...defaultManagers.map((defaultMgr) => ({ + label: defaultMgr.displayName, + description: getDescription(defaultMgr), + id: defaultMgr.id, + })), + { + label: '', + kind: QuickPickItemKind.Separator, + }, + ); + } + items.push( + ...managers + .filter((m) => !defaultManagers?.includes(m)) + .map((m) => ({ + label: m.displayName, + description: getDescription(m), + id: m.id, + })), + ); + const item = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Managers.selectPackageManager, + ignoreFocusOut: true, + }); + return (item as QuickPickItem & { id: string })?.id; +} + +export async function pickWorkspaceFolder(showBackButton = true): Promise { + const folders = workspace.workspaceFolders; + if (!folders || folders.length === 0) { + return undefined; + } + if (folders.length === 1) { + return folders[0]; + } + const items = folders.map((f) => ({ + label: f.name, + description: f.uri.fsPath, + folder: f, + })); + + const selected = await showQuickPickWithButtons(items, { + placeHolder: 'Select a workspace folder', + ignoreFocusOut: true, + showBackButton, + }); + if (!selected) { + return undefined; + } + const selectedItem = Array.isArray(selected) ? selected[0] : selected; + return selectedItem?.folder; +} +export async function pickCreator(creators: PythonProjectCreator[]): Promise { + if (creators.length === 0) { + return; + } + + if (creators.length === 1) { + return creators[0]; + } + + // First level menu + const autoFindCreator = creators.find((c) => c.name === 'autoProjects'); + const existingProjectsCreator = creators.find((c) => c.name === 'existingProjects'); + + const items: QuickPickItem[] = [ + { + label: 'Auto Find', + description: autoFindCreator?.description ?? 'Automatically find Python projects', + }, + { + label: 'Select Existing', + description: existingProjectsCreator?.description ?? 'Select existing Python projects', + }, + { + label: 'Create New', + description: 'Create a Python project from a template', + }, + ]; + + const selected = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Managers.selectProjectCreator, + ignoreFocusOut: true, + showBackButton: true, + }); + + if (!selected) { + return undefined; + } + + // Return appropriate creator based on selection + // Handle case where selected could be an array (should not happen, but for type safety) + const selectedItem = Array.isArray(selected) ? selected[0] : selected; + if (!selectedItem) { + return undefined; + } + switch (selectedItem.label) { + case 'Auto Find': + return autoFindCreator; + case 'Select Existing': + return existingProjectsCreator; + case 'Create New': + return newProjectSelection(creators); + } + + return undefined; +} + +export async function newProjectSelection(creators: PythonProjectCreator[]): Promise { + const otherCreators = creators.filter((c) => c.name !== 'autoProjects' && c.name !== 'existingProjects'); + + // Show second level menu for other creators + if (otherCreators.length === 0) { + return undefined; + } + const newItems: (QuickPickItem & { c: PythonProjectCreator })[] = otherCreators.map((c) => ({ + label: c.displayName ?? c.name, + description: c.description, + c: c, + })); + try { + const newSelected = await showQuickPickWithButtons(newItems, { + placeHolder: 'Select project type for new project', + ignoreFocusOut: true, + showBackButton: true, + }); + + if (!newSelected) { + // User cancelled the picker + return undefined; + } + // Handle back button + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((newSelected as any)?.kind === -1 || (newSelected as any)?.back === true) { + // User pressed the back button, re-show the first menu + return pickCreator(creators); + } + + // Handle case where newSelected could be an array (should not happen, but for type safety) + const selectedCreator = Array.isArray(newSelected) ? newSelected[0] : newSelected; + return selectedCreator?.c; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + await commands.executeCommand('python-envs.addPythonProject'); + } + } +} diff --git a/src/common/pickers/projects.ts b/src/common/pickers/projects.ts new file mode 100644 index 0000000..19e93f6 --- /dev/null +++ b/src/common/pickers/projects.ts @@ -0,0 +1,56 @@ +import path from 'path'; +import { QuickPickItem } from 'vscode'; +import { PythonProject } from '../../api'; +import { showQuickPick, showQuickPickWithButtons } from '../window.apis'; +import { Pickers } from '../localize'; + +interface ProjectQuickPickItem extends QuickPickItem { + project: PythonProject; +} + +export async function pickProject(projects: ReadonlyArray): Promise { + if (projects.length > 1) { + const items: ProjectQuickPickItem[] = projects.map((pw) => ({ + label: path.basename(pw.uri.fsPath), + description: pw.uri.fsPath, + project: pw, + })); + const item = await showQuickPick(items, { + placeHolder: Pickers.Project.selectProject, + ignoreFocusOut: true, + }); + if (item) { + return item.project; + } + } else if (projects.length === 1) { + return projects[0]; + } + return undefined; +} + +export async function pickProjectMany( + projects: readonly PythonProject[], + showBackButton?: boolean, +): Promise { + if (projects.length > 1) { + const items: ProjectQuickPickItem[] = projects.map((pw) => ({ + label: path.basename(pw.uri.fsPath), + description: pw.uri.fsPath, + project: pw, + })); + const item = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Project.selectProjects, + ignoreFocusOut: true, + canPickMany: true, + showBackButton: showBackButton, + }); + if (Array.isArray(item)) { + return item.map((p) => p.project); + } + } else if (projects.length === 1) { + return [...projects]; + } else if (projects.length === 0) { + return []; + } + return undefined; +} diff --git a/src/common/telemetry/constants.ts b/src/common/telemetry/constants.ts new file mode 100644 index 0000000..d9ae5d9 --- /dev/null +++ b/src/common/telemetry/constants.ts @@ -0,0 +1,123 @@ +export enum EventNames { + EXTENSION_ACTIVATION_DURATION = 'EXTENSION.ACTIVATION_DURATION', + EXTENSION_MANAGER_REGISTRATION_DURATION = 'EXTENSION.MANAGER_REGISTRATION_DURATION', + + ENVIRONMENT_MANAGER_REGISTERED = 'ENVIRONMENT_MANAGER.REGISTERED', + PACKAGE_MANAGER_REGISTERED = 'PACKAGE_MANAGER.REGISTERED', + ENVIRONMENT_MANAGER_SELECTED = 'ENVIRONMENT_MANAGER.SELECTED', + PACKAGE_MANAGER_SELECTED = 'PACKAGE_MANAGER.SELECTED', + + VENV_USING_UV = 'VENV.USING_UV', + VENV_CREATION = 'VENV.CREATION', + + PACKAGE_MANAGEMENT = 'PACKAGE_MANAGEMENT', + ADD_PROJECT = 'ADD_PROJECT', + /** + * Telemetry event for when a Python environment is created via command. + * Properties: + * - manager: string (the id of the environment manager used, or 'none') + * - triggeredLocation: string (where the create command is called from) + */ + CREATE_ENVIRONMENT = 'CREATE_ENVIRONMENT', +} + +// Map all events to their properties +export interface IEventNamePropertyMapping { + /* __GDPR__ + "extension.activation_duration": { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.EXTENSION_ACTIVATION_DURATION]: never | undefined; + /* __GDPR__ + "extension.manager_registration_duration": { + "duration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION]: never | undefined; + + /* __GDPR__ + "environment_manager.registered": { + "managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.ENVIRONMENT_MANAGER_REGISTERED]: { + managerId: string; + }; + + /* __GDPR__ + "package_manager.registered": { + "managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.PACKAGE_MANAGER_REGISTERED]: { + managerId: string; + }; + + /* __GDPR__ + "environment_manager.selected": { + "managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.ENVIRONMENT_MANAGER_SELECTED]: { + managerId: string; + }; + + /* __GDPR__ + "package_manager.selected": { + "managerId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.PACKAGE_MANAGER_SELECTED]: { + managerId: string; + }; + + /* __GDPR__ + "venv.using_uv": {"owner": "eleanorjboyd" } + */ + [EventNames.VENV_USING_UV]: never | undefined /* __GDPR__ + "venv.creation": { + "creationType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */; + [EventNames.VENV_CREATION]: { + creationType: 'quick' | 'custom'; + }; + + /* __GDPR__ + "package_management": { + "managerId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "result": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.PACKAGE_MANAGEMENT]: { + managerId: string; + result: 'success' | 'error' | 'cancelled'; + }; + + /* __GDPR__ + "add_project": { + "template": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "quickCreate": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "totalProjectCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "triggeredLocation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.ADD_PROJECT]: { + template: string; + quickCreate: boolean; + totalProjectCount: number; + triggeredLocation: 'templateCreate' | 'add' | 'addGivenResource'; + }; + + /* __GDPR__ + "create_environment": { + "manager": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" }, + "triggeredLocation": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "eleanorjboyd" } + } + */ + [EventNames.CREATE_ENVIRONMENT]: { + manager: string; + triggeredLocation: string; + }; +} diff --git a/src/common/telemetry/helpers.ts b/src/common/telemetry/helpers.ts new file mode 100644 index 0000000..d2a43c6 --- /dev/null +++ b/src/common/telemetry/helpers.ts @@ -0,0 +1,28 @@ +import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../../features/settings/settingHelpers'; +import { PythonProjectManager } from '../../internal.api'; +import { EventNames } from './constants'; +import { sendTelemetryEvent } from './sender'; + +export function sendManagerSelectionTelemetry(pm: PythonProjectManager) { + const ems: Set = new Set(); + const ps: Set = new Set(); + pm.getProjects().forEach((project) => { + const m = getDefaultEnvManagerSetting(pm, project.uri); + if (m) { + ems.add(m); + } + + const p = getDefaultPkgManagerSetting(pm, project.uri); + if (p) { + ps.add(p); + } + }); + + ems.forEach((em) => { + sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_SELECTED, undefined, { managerId: em }); + }); + + ps.forEach((pkg) => { + sendTelemetryEvent(EventNames.PACKAGE_MANAGER_SELECTED, undefined, { managerId: pkg }); + }); +} diff --git a/src/common/telemetry/reporter.ts b/src/common/telemetry/reporter.ts new file mode 100644 index 0000000..251349d --- /dev/null +++ b/src/common/telemetry/reporter.ts @@ -0,0 +1,23 @@ +import type TelemetryReporter from '@vscode/extension-telemetry'; + +class ReporterImpl { + private static telemetryReporter: TelemetryReporter | undefined; + static getTelemetryReporter() { + const tel = require('@vscode/extension-telemetry'); + const Reporter = tel.default as typeof TelemetryReporter; + ReporterImpl.telemetryReporter = new Reporter( + '0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255', + [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ], + ); + + return ReporterImpl.telemetryReporter; + } +} + +export function getTelemetryReporter() { + return ReporterImpl.getTelemetryReporter(); +} diff --git a/src/common/telemetry/sender.ts b/src/common/telemetry/sender.ts new file mode 100644 index 0000000..80874b1 --- /dev/null +++ b/src/common/telemetry/sender.ts @@ -0,0 +1,140 @@ +import type { IEventNamePropertyMapping } from './constants'; +import { StopWatch } from '../stopWatch'; +import { isTestExecution } from '../utils/testing'; +import { getTelemetryReporter } from './reporter'; +import { isPromise } from 'util/types'; + +type FailedEventType = { failed: true }; + +function isTelemetrySupported(): boolean { + try { + const vsc = require('vscode'); + const reporter = require('@vscode/extension-telemetry'); + return !!vsc && !!reporter; + } catch { + return false; + } +} + +export function sendTelemetryEvent

( + eventName: E, + measuresOrDurationMs?: Record | number, + properties?: P[E], + ex?: Error, +): void { + if (isTestExecution() || !isTelemetrySupported()) { + return; + } + const reporter = getTelemetryReporter(); + const measures = + typeof measuresOrDurationMs === 'number' ? { duration: measuresOrDurationMs } : measuresOrDurationMs; + + const customProperties: Record = {}; + const eventNameSent = eventName as string; + + if (properties) { + const data = properties as Record; + Object.entries(data).forEach(([prop, value]) => { + if (value === null || value === undefined) { + return; + } + + try { + customProperties[prop] = typeof value === 'object' ? 'object' : String(value); + } catch (exception) { + console.error(`Failed to serialize ${prop} for ${String(eventName)}`, exception); + } + }); + } + + if (ex) { + const errorProps = { + errorName: ex.name, + errorStack: ex.stack ?? '', + }; + Object.assign(customProperties, errorProps); + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); + } else { + reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); + } +} + +type TypedMethodDescriptor = ( + target: unknown, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => TypedPropertyDescriptor | void; + +export function captureTelemetry( + eventName: E, + properties?: P[E], + captureDuration = true, + failureEventName?: E, + lazyProperties?: (obj: This, result?: unknown) => P[E], + lazyMeasures?: (obj: This, result?: unknown) => Record, +): TypedMethodDescriptor<(this: This, ...args: unknown[]) => unknown> { + return function ( + _target: unknown, + _propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor<(this: This, ...args: unknown[]) => unknown>, + ) { + const originalMethod = descriptor.value!; + + descriptor.value = function (this: This, ...args: unknown[]) { + if (!captureDuration && !lazyProperties && !lazyMeasures) { + sendTelemetryEvent(eventName, undefined, properties); + return originalMethod.apply(this, args); + } + + const getProps = (result?: unknown) => + lazyProperties ? { ...properties, ...lazyProperties(this, result) } : properties; + const stopWatch = captureDuration ? new StopWatch() : undefined; + const getMeasures = (result?: unknown) => { + const measures = stopWatch ? { duration: stopWatch.elapsedTime } : undefined; + return lazyMeasures ? { ...measures, ...lazyMeasures(this, result) } : measures; + }; + + const result = originalMethod.apply(this, args); + + if (result && isPromise(result)) { + return result + .then((data) => { + sendTelemetryEvent(eventName, getMeasures(data), getProps(data)); + return data; + }) + .catch((ex) => { + const failedProps: P[E] = { ...getProps(), failed: true } as P[E] & FailedEventType; + sendTelemetryEvent(failureEventName || eventName, getMeasures(), failedProps, ex); + return Promise.reject(ex); + }); + } else { + sendTelemetryEvent(eventName, getMeasures(result), getProps(result)); + return result; + } + }; + + return descriptor; + }; +} + +export function sendTelemetryWhenDone

( + eventName: E, + promise: Promise | Thenable, + stopWatch: StopWatch = new StopWatch(), + properties?: P[E], +): void { + if (typeof promise.then === 'function') { + promise.then( + (data) => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties); + return data; + }, + (ex) => { + sendTelemetryEvent(eventName, stopWatch.elapsedTime, properties, ex); + return Promise.reject(ex); + }, + ); + } else { + throw new Error('Method is neither a Promise nor a Thenable'); + } +} diff --git a/src/common/utils/asyncUtils.ts b/src/common/utils/asyncUtils.ts new file mode 100644 index 0000000..4bb79f8 --- /dev/null +++ b/src/common/utils/asyncUtils.ts @@ -0,0 +1,3 @@ +export async function sleep(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} diff --git a/src/common/utils/debounce.ts b/src/common/utils/debounce.ts index bbc44f2..1b19706 100644 --- a/src/common/utils/debounce.ts +++ b/src/common/utils/debounce.ts @@ -5,7 +5,7 @@ export interface SimpleDebounce { class SimpleDebounceImpl { private timeout: NodeJS.Timeout | undefined; - constructor(private readonly delay: number, private readonly callback: () => void) {} + constructor(private readonly ms: number, private readonly callback: () => void) {} public trigger() { if (this.timeout) { @@ -13,10 +13,10 @@ class SimpleDebounceImpl { } this.timeout = setTimeout(() => { this.callback(); - }, this.delay); + }, this.ms); } } -export function createSimpleDebounce(delay: number, callback: () => void): SimpleDebounce { - return new SimpleDebounceImpl(delay, callback); +export function createSimpleDebounce(ms: number, callback: () => void): SimpleDebounce { + return new SimpleDebounceImpl(ms, callback); } diff --git a/src/common/utils/fileNameUtils.ts b/src/common/utils/fileNameUtils.ts index b55e7b2..1bfc65f 100644 --- a/src/common/utils/fileNameUtils.ts +++ b/src/common/utils/fileNameUtils.ts @@ -1,7 +1,8 @@ import * as path from 'path'; import * as fsapi from 'fs-extra'; import { KNOWN_FILES, KNOWN_TEMPLATE_ENDINGS } from '../constants'; -import { Uri, workspace } from 'vscode'; +import { Uri } from 'vscode'; +import { getWorkspaceFolders } from '../workspace.apis'; export function isPythonProjectFile(fileName: string): boolean { const baseName = path.basename(fileName).toLowerCase(); @@ -20,14 +21,15 @@ export async function getAbsolutePath(fsPath: string): Promise return Uri.file(fsPath); } - if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) { - if (workspace.workspaceFolders.length === 1) { - const absPath = path.resolve(workspace.workspaceFolders[0].uri.fsPath, fsPath); + const workspaceFolders = getWorkspaceFolders() ?? []; + if (workspaceFolders.length > 0) { + if (workspaceFolders.length === 1) { + const absPath = path.resolve(workspaceFolders[0].uri.fsPath, fsPath); if (await fsapi.pathExists(absPath)) { return Uri.file(absPath); } } else { - const workspaces = Array.from(workspace.workspaceFolders) + const workspaces = Array.from(workspaceFolders) .sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length) .reverse(); for (const folder of workspaces) { diff --git a/src/common/utils/frameUtils.ts b/src/common/utils/frameUtils.ts new file mode 100644 index 0000000..479045b --- /dev/null +++ b/src/common/utils/frameUtils.ts @@ -0,0 +1,88 @@ +import { Uri } from 'vscode'; +import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../constants'; +import { parseStack } from '../errors/utils'; +import { allExtensions, getExtension } from '../extension.apis'; +import { normalizePath } from './pathUtils'; +interface FrameData { + filePath: string; + functionName: string; +} + +function getFrameData(): FrameData[] { + const frames = parseStack(new Error()); + return frames.map((frame) => ({ + filePath: frame.getFileName(), + functionName: frame.getFunctionName(), + })); +} + +function getPathFromFrame(frame: FrameData): string { + if (frame.filePath && frame.filePath.startsWith('file://')) { + return Uri.parse(frame.filePath).fsPath; + } + return frame.filePath; +} + +export function getCallingExtension(): string { + const pythonExts = [ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID]; + const extensions = allExtensions(); + const otherExts = extensions.filter((ext) => !pythonExts.includes(ext.id)); + const frames = getFrameData(); + + const registerEnvManagerFrameIndex = frames.findIndex( + (frame) => + frame.functionName && + (frame.functionName.includes('registerEnvironmentManager') || + frame.functionName.includes('registerPackageManager')), + ); + + const relevantFrames = + registerEnvManagerFrameIndex !== -1 ? frames.slice(registerEnvManagerFrameIndex + 1) : frames; + + const filePaths: string[] = []; + for (const frame of relevantFrames) { + if (!frame || !frame.filePath) { + continue; + } + const filePath = normalizePath(getPathFromFrame(frame)); + if (!filePath) { + continue; + } + + if (filePath.toLowerCase().endsWith('extensionhostprocess.js')) { + continue; + } + + if (filePath.startsWith('node:')) { + continue; + } + + filePaths.push(filePath); + + const ext = otherExts.find((ext) => filePath.includes(ext.id)); + if (ext) { + return ext.id; + } + } + + const envExt = getExtension(ENVS_EXTENSION_ID); + const pythonExt = getExtension(PYTHON_EXTENSION_ID); + if (!envExt || !pythonExt) { + throw new Error('Something went wrong with feature registration'); + } + const envsExtPath = normalizePath(envExt.extensionPath); + + if (filePaths.every((filePath) => filePath.startsWith(envsExtPath))) { + return PYTHON_EXTENSION_ID; + } + + for (const ext of otherExts) { + const extPath = normalizePath(ext.extensionPath); + if (filePaths.some((filePath) => filePath.startsWith(extPath))) { + return ext.id; + } + } + + // Fallback - we're likely being called from Python extension in conda registration + return PYTHON_EXTENSION_ID; +} diff --git a/src/common/utils/internalVariables.ts b/src/common/utils/internalVariables.ts new file mode 100644 index 0000000..22d5888 --- /dev/null +++ b/src/common/utils/internalVariables.ts @@ -0,0 +1,40 @@ +import { Uri } from 'vscode'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../workspace.apis'; + +export function resolveVariables(value: string, project?: Uri, env?: { [key: string]: string }): string { + const substitutions = new Map(); + const home = process.env.HOME || process.env.USERPROFILE; + if (home) { + substitutions.set('${userHome}', home); + } + + if (project) { + substitutions.set('${pythonProject}', project.fsPath); + } + + const workspace = project ? getWorkspaceFolder(project) : undefined; + if (workspace) { + substitutions.set('${workspaceFolder}', workspace.uri.fsPath); + } + substitutions.set('${cwd}', process.cwd()); + (getWorkspaceFolders() ?? []).forEach((w) => { + substitutions.set('${workspaceFolder:' + w.name + '}', w.uri.fsPath); + }); + + const substEnv = env || process.env; + if (substEnv) { + for (const [key, value] of Object.entries(substEnv)) { + if (value && key.length > 0) { + substitutions.set('${env:' + key + '}', value); + } + } + } + + let result = value; + substitutions.forEach((v, k) => { + while (k.length > 0 && result.indexOf(k) >= 0) { + result = result.replace(k, v); + } + }); + return result; +} diff --git a/src/common/utils/pathUtils.ts b/src/common/utils/pathUtils.ts new file mode 100644 index 0000000..d398828 --- /dev/null +++ b/src/common/utils/pathUtils.ts @@ -0,0 +1,104 @@ +import * as os from 'os'; +import * as path from 'path'; +import { NotebookCell, NotebookDocument, Uri, workspace } from 'vscode'; +import { isWindows } from './platformUtils'; + +export function checkUri(scope?: Uri | Uri[] | string): Uri | Uri[] | string | undefined { + if (!scope) { + return undefined; + } + + if (Array.isArray(scope)) { + // if the scope is an array, all items must be Uri, check each item + return scope.map((item) => { + const s = checkUri(item); + if (s instanceof Uri) { + return s; + } + throw new Error('Invalid entry, expected Uri.'); + }); + } + + if (scope instanceof Uri) { + if (scope.scheme === 'vscode-notebook-cell') { + const matchingDoc = workspace.notebookDocuments.find((doc) => findCell(scope, doc)); + // If we find a matching notebook document, return the Uri of the cell. + return matchingDoc ? matchingDoc.uri : scope; + } + } + return scope; +} + +/** + * Find a notebook document by cell Uri. + */ +export function findCell(cellUri: Uri, notebook: NotebookDocument): NotebookCell | undefined { + // Fragment is not unique to a notebook, hence ensure we compare the path as well. + return notebook.getCells().find((cell) => { + return isEqual(cell.document.uri, cellUri); + }); +} +function isEqual(uri1: Uri | undefined, uri2: Uri | undefined): boolean { + if (uri1 === uri2) { + return true; + } + if (!uri1 || !uri2) { + return false; + } + return getComparisonKey(uri1) === getComparisonKey(uri2); +} + +function getComparisonKey(uri: Uri): string { + return uri + .with({ + path: isWindows() ? uri.path.toLowerCase() : uri.path, + fragment: undefined, + }) + .toString(); +} + +export function normalizePath(fsPath: string): string { + const path1 = fsPath.replace(/\\/g, '/'); + if (isWindows()) { + return path1.toLowerCase(); + } + return path1; +} + +export function getResourceUri(resourcePath: string, root?: string): Uri | undefined { + try { + if (!resourcePath) { + return undefined; + } + + const normalizedPath = normalizePath(resourcePath); + if (normalizedPath.includes('://')) { + return Uri.parse(normalizedPath); + } + + if (!path.isAbsolute(resourcePath) && root) { + const absolutePath = path.resolve(root, resourcePath); + return Uri.file(absolutePath); + } + return Uri.file(resourcePath); + } catch (_err) { + return undefined; + } +} + +export function untildify(path: string): string { + return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); +} + +export function getUserHomeDir(): string { + return os.homedir(); +} + +/** + * Applies untildify to an array of paths + * @param paths Array of potentially tilde-containing paths + * @returns Array of expanded paths + */ +export function untildifyArray(paths: string[]): string[] { + return paths.map((p) => untildify(p)); +} diff --git a/src/common/utils/platformUtils.ts b/src/common/utils/platformUtils.ts new file mode 100644 index 0000000..bc11fd8 --- /dev/null +++ b/src/common/utils/platformUtils.ts @@ -0,0 +1,3 @@ +export function isWindows(): boolean { + return process.platform === 'win32'; +} diff --git a/src/common/utils/pythonPath.ts b/src/common/utils/pythonPath.ts new file mode 100644 index 0000000..33e6f6a --- /dev/null +++ b/src/common/utils/pythonPath.ts @@ -0,0 +1,80 @@ +import { Uri, Progress, CancellationToken } from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { InternalEnvironmentManager } from '../../internal.api'; +import { traceVerbose, traceError } from '../logging'; +import { PYTHON_EXTENSION_ID } from '../constants'; +import { showErrorMessage } from '../window.apis'; + +const priorityOrder = [ + `${PYTHON_EXTENSION_ID}:pyenv`, + `${PYTHON_EXTENSION_ID}:pixi`, + `${PYTHON_EXTENSION_ID}:conda`, + `${PYTHON_EXTENSION_ID}:pipenv`, + `${PYTHON_EXTENSION_ID}:poetry`, + `${PYTHON_EXTENSION_ID}:activestate`, + `${PYTHON_EXTENSION_ID}:hatch`, + `${PYTHON_EXTENSION_ID}:venv`, + `${PYTHON_EXTENSION_ID}:system`, +]; +function sortManagersByPriority(managers: InternalEnvironmentManager[]): InternalEnvironmentManager[] { + return managers.sort((a, b) => { + const aIndex = priorityOrder.indexOf(a.id); + const bIndex = priorityOrder.indexOf(b.id); + if (aIndex === -1 && bIndex === -1) { + return 0; + } + if (aIndex === -1) { + return 1; + } + if (bIndex === -1) { + return -1; + } + return aIndex - bIndex; + }); +} + +export async function handlePythonPath( + interpreterUri: Uri, + managers: InternalEnvironmentManager[], + projectEnvManagers: InternalEnvironmentManager[], + reporter?: Progress<{ message?: string; increment?: number }>, + token?: CancellationToken, +): Promise { + // Use the managers user has set for the project first. Likely, these + // managers are the ones that should be used. + for (const manager of sortManagersByPriority(projectEnvManagers)) { + if (token?.isCancellationRequested) { + return; + } + reporter?.report({ message: `Checking ${manager.displayName}` }); + traceVerbose(`Checking ${manager.displayName} (${manager.id}) for ${interpreterUri.fsPath}`); + const env = await manager.resolve(interpreterUri); + if (env) { + traceVerbose(`Using ${manager.displayName} (${manager.id}) to handle ${interpreterUri.fsPath}`); + return env; + } + traceVerbose(`Manager ${manager.displayName} (${manager.id}) cannot handle ${interpreterUri.fsPath}`); + } + + // If the project managers cannot handle the interpreter, then try all the managers + // that user has installed. Excluding anything that is already checked. + const checkedIds = projectEnvManagers.map((m) => m.id); + const filtered = managers.filter((m) => !checkedIds.includes(m.id)); + + for (const manager of sortManagersByPriority(filtered)) { + if (token?.isCancellationRequested) { + return; + } + reporter?.report({ message: `Checking ${manager.displayName}` }); + traceVerbose(`Checking ${manager.displayName} (${manager.id}) for ${interpreterUri.fsPath}`); + const env = await manager.resolve(interpreterUri); + if (env) { + traceVerbose(`Using ${manager.displayName} (${manager.id}) to handle ${interpreterUri.fsPath}`); + return env; + } + } + + traceError(`Unable to handle ${interpreterUri.fsPath}`); + showErrorMessage(`Unable to handle ${interpreterUri.fsPath}`); + return undefined; +} diff --git a/src/common/utils/testing.ts b/src/common/utils/testing.ts new file mode 100644 index 0000000..dd24133 --- /dev/null +++ b/src/common/utils/testing.ts @@ -0,0 +1,3 @@ +export function isTestExecution(): boolean { + return !!process.env.VSC_PYTHON_CI_TEST; +} diff --git a/src/common/window.apis.ts b/src/common/window.apis.ts index f2da347..51a2032 100644 --- a/src/common/window.apis.ts +++ b/src/common/window.apis.ts @@ -1,7 +1,19 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { CancellationToken, Disposable, ExtensionTerminalOptions, + FileDecorationProvider, + InputBox, + InputBoxOptions, + LogOutputChannel, + MessageItem, + MessageOptions, + OpenDialogOptions, + OutputChannel, + Progress, + ProgressOptions, + QuickInputButton, QuickInputButtons, QuickPick, QuickPickItem, @@ -12,10 +24,12 @@ import { Terminal, TerminalOptions, TerminalShellExecutionEndEvent, + TerminalShellExecutionStartEvent, TerminalShellIntegrationChangeEvent, TextEditor, Uri, window, + WindowState, } from 'vscode'; import { createDeferred } from './utils/deferred'; @@ -35,6 +49,10 @@ export function onDidChangeTerminalShellIntegration( return window.onDidChangeTerminalShellIntegration(listener, thisArgs, disposables); } +export function showOpenDialog(options?: OpenDialogOptions): Thenable { + return window.showOpenDialog(options); +} + export function terminals(): readonly Terminal[] { return window.terminals; } @@ -43,6 +61,38 @@ export function activeTerminal(): Terminal | undefined { return window.activeTerminal; } +export function activeTerminalShellIntegration() { + return window.activeTerminal?.shellIntegration; +} + +export function activeTextEditor(): TextEditor | undefined { + return window.activeTextEditor; +} + +export function onDidChangeActiveTerminal( + listener: (e: Terminal | undefined) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidChangeActiveTerminal(listener, thisArgs, disposables); +} + +export function onDidChangeActiveTextEditor( + listener: (e: TextEditor | undefined) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidChangeActiveTextEditor(listener, thisArgs, disposables); +} + +export function onDidStartTerminalShellExecution( + listener: (e: TerminalShellExecutionStartEvent) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidStartTerminalShellExecution(listener, thisArgs, disposables); +} + export function onDidEndTerminalShellExecution( listener: (e: TerminalShellExecutionEndEvent) => any, thisArgs?: any, @@ -67,38 +117,79 @@ export function onDidCloseTerminal( return window.onDidCloseTerminal(listener, thisArgs, disposables); } +export function onDidChangeTerminalState( + listener: (e: Terminal) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidChangeTerminalState(listener, thisArgs, disposables); +} + export function showTextDocument(uri: Uri): Thenable { return window.showTextDocument(uri); } +export interface QuickPickButtonEvent { + readonly item: T | readonly T[] | undefined; + readonly button: QuickInputButton; +} + +export function showQuickPick( + items: readonly T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, +): Thenable { + return window.showQuickPick(items, options, token); +} + +export function withProgress( + options: ProgressOptions, + task: ( + progress: Progress<{ + message?: string; + increment?: number; + }>, + token: CancellationToken, + ) => Thenable, +): Thenable { + return window.withProgress(options, task); +} + export async function showQuickPickWithButtons( items: readonly T[], - options?: QuickPickOptions & { showBackButton?: boolean }, + options?: QuickPickOptions & { showBackButton?: boolean; buttons?: QuickInputButton[]; selected?: T[] }, token?: CancellationToken, itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, ): Promise { const quickPick: QuickPick = window.createQuickPick(); const disposables: Disposable[] = [quickPick]; + const deferred = createDeferred(); quickPick.items = items; - if (options?.showBackButton) { - quickPick.buttons = [QuickInputButtons.Back]; - } quickPick.canSelectMany = options?.canPickMany ?? false; quickPick.ignoreFocusOut = options?.ignoreFocusOut ?? false; quickPick.matchOnDescription = options?.matchOnDescription ?? false; quickPick.matchOnDetail = options?.matchOnDetail ?? false; quickPick.placeholder = options?.placeHolder; quickPick.title = options?.title; + quickPick.selectedItems = options?.selected ?? []; - const deferred = createDeferred(); + if (options?.showBackButton) { + quickPick.buttons = [QuickInputButtons.Back]; + } + + if (options?.buttons) { + quickPick.buttons = [...quickPick.buttons, ...options.buttons]; + } disposables.push( - quickPick, - quickPick.onDidTriggerButton((item) => { - if (item === QuickInputButtons.Back) { + quickPick.onDidTriggerButton((button) => { + if (button === QuickInputButtons.Back) { deferred.reject(QuickInputButtons.Back); quickPick.hide(); + } else if (options?.buttons?.includes(button)) { + deferred.reject({ item: quickPick.selectedItems, button }); + quickPick.hide(); } }), quickPick.onDidAccept(() => { @@ -138,3 +229,143 @@ export async function showQuickPickWithButtons( disposables.forEach((d) => d.dispose()); } } + +export async function showInputBoxWithButtons( + options?: InputBoxOptions & { showBackButton?: boolean }, +): Promise { + const inputBox: InputBox = window.createInputBox(); + const disposables: Disposable[] = [inputBox]; + const deferred = createDeferred(); + + inputBox.placeholder = options?.placeHolder; + inputBox.title = options?.title; + inputBox.value = options?.value ?? ''; + inputBox.ignoreFocusOut = options?.ignoreFocusOut ?? false; + inputBox.password = options?.password ?? false; + inputBox.prompt = options?.prompt; + + if (options?.valueSelection) { + inputBox.valueSelection = options?.valueSelection; + } + + if (options?.showBackButton) { + inputBox.buttons = [QuickInputButtons.Back]; + } + + disposables.push( + inputBox.onDidTriggerButton((button) => { + if (button === QuickInputButtons.Back) { + deferred.reject(QuickInputButtons.Back); + inputBox.hide(); + } + }), + inputBox.onDidAccept(async () => { + if (!deferred.completed) { + let isValid = true; + if (options?.validateInput) { + const validation = await options.validateInput(inputBox.value); + isValid = validation === null || validation === undefined; + if (!isValid) { + inputBox.validationMessage = typeof validation === 'string' ? validation : 'Invalid input'; + return; // Do not resolve, keep the input box open + } + } + deferred.resolve(inputBox.value); + inputBox.hide(); + } + }), + inputBox.onDidHide(() => { + if (!deferred.completed) { + deferred.resolve(undefined); + } + }), + inputBox.onDidChangeValue(async (value) => { + if (options?.validateInput) { + const validation = await options?.validateInput(value); + if (validation === null || validation === undefined) { + inputBox.validationMessage = undefined; + } else { + inputBox.validationMessage = validation; + } + } + }), + ); + + inputBox.show(); + + try { + return await deferred.promise; + } finally { + disposables.forEach((d) => d.dispose()); + } +} + +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable { + return window.showInformationMessage(message, options, ...items); +} + +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable { + return window.showErrorMessage(message, options, ...items); +} + +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: any[]): Thenable { + return window.showWarningMessage(message, ...items); +} + +export function showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { + return window.showInputBox(options, token); +} + +export function createOutputChannel(name: string, languageId?: string): OutputChannel { + return window.createOutputChannel(name, languageId); +} + +export function createLogOutputChannel(name: string): LogOutputChannel { + return window.createOutputChannel(name, { log: true }); +} + +export function registerFileDecorationProvider(provider: FileDecorationProvider): Disposable { + return window.registerFileDecorationProvider(provider); +} + +export function onDidChangeWindowState( + listener: (e: WindowState) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return window.onDidChangeWindowState(listener, thisArgs, disposables); +} diff --git a/src/common/workbenchCommands.ts b/src/common/workbenchCommands.ts new file mode 100644 index 0000000..0efe870 --- /dev/null +++ b/src/common/workbenchCommands.ts @@ -0,0 +1,12 @@ +import { commands, Uri } from 'vscode'; + +export async function installExtension( + extensionId: Uri | string, + options?: { + installOnlyNewlyAddedFromExtensionPackVSIX?: boolean; + installPreReleaseVersion?: boolean; + donotSync?: boolean; + }, +): Promise { + await commands.executeCommand('workbench.extensions.installExtension', extensionId, options); +} diff --git a/src/common/workspace.apis.ts b/src/common/workspace.apis.ts index c4236d2..213c0b8 100644 --- a/src/common/workspace.apis.ts +++ b/src/common/workspace.apis.ts @@ -1,8 +1,11 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { CancellationToken, ConfigurationChangeEvent, ConfigurationScope, Disposable, + FileDeleteEvent, + FileSystemWatcher, GlobPattern, Uri, workspace, @@ -19,6 +22,10 @@ export function getWorkspaceFolders(): readonly WorkspaceFolder[] | undefined { return workspace.workspaceFolders; } +export function getWorkspaceFile(): Uri | undefined { + return workspace.workspaceFile; +} + export function getConfiguration(section?: string, scope?: ConfigurationScope | null): WorkspaceConfiguration { return workspace.getConfiguration(section, scope); } @@ -39,3 +46,20 @@ export function findFiles( ): Thenable { return workspace.findFiles(include, exclude, maxResults, token); } + +export function createFileSystemWatcher( + globPattern: GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, +): FileSystemWatcher { + return workspace.createFileSystemWatcher(globPattern, ignoreCreateEvents, ignoreChangeEvents, ignoreDeleteEvents); +} + +export function onDidDeleteFiles( + listener: (e: FileDeleteEvent) => any, + thisArgs?: any, + disposables?: Disposable[], +): Disposable { + return workspace.onDidDeleteFiles(listener, thisArgs, disposables); +} diff --git a/src/common/workspace.fs.apis.ts b/src/common/workspace.fs.apis.ts new file mode 100644 index 0000000..f10e7ad --- /dev/null +++ b/src/common/workspace.fs.apis.ts @@ -0,0 +1,9 @@ +import { FileStat, Uri, workspace } from 'vscode'; + +export function readFile(uri: Uri): Thenable { + return workspace.fs.readFile(uri); +} + +export function stat(uri: Uri): Thenable { + return workspace.fs.stat(uri); +} diff --git a/src/extension.ts b/src/extension.ts index fce9638..8024024 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,92 +1,205 @@ -import { window, commands, ExtensionContext, LogOutputChannel, TextEditor } from 'vscode'; +import { commands, ExtensionContext, LogOutputChannel, Terminal, Uri, window } from 'vscode'; +import { version as extensionVersion } from '../package.json'; +import { PythonEnvironment, PythonEnvironmentApi, PythonProjectCreator } from './api'; +import { ensureCorrectVersion } from './common/extVersion'; +import { registerLogger, traceError, traceInfo, traceVerbose, traceWarn } from './common/logging'; +import { clearPersistentState, setPersistentState } from './common/persistentState'; +import { newProjectSelection } from './common/pickers/managers'; +import { StopWatch } from './common/stopWatch'; +import { EventNames } from './common/telemetry/constants'; +import { sendManagerSelectionTelemetry } from './common/telemetry/helpers'; +import { sendTelemetryEvent } from './common/telemetry/sender'; +import { createDeferred } from './common/utils/deferred'; -import { PythonEnvironmentManagers } from './features/envManagers'; -import { registerLogger } from './common/logging'; -import { EnvManagerView } from './features/views/envManagersView'; import { - addPythonProject, + activeTerminal, + createLogOutputChannel, + onDidChangeActiveTerminal, + onDidChangeTerminalShellIntegration, +} from './common/window.apis'; +import { getConfiguration } from './common/workspace.apis'; +import { createManagerReady } from './features/common/managerReady'; +import { AutoFindProjects } from './features/creators/autoFindProjects'; +import { ExistingProjects } from './features/creators/existingProjects'; +import { NewPackageProject } from './features/creators/newPackageProject'; +import { NewScriptProject } from './features/creators/newScriptProject'; +import { ProjectCreatorsImpl } from './features/creators/projectCreators'; +import { + addPythonProjectCommand, + copyPathToClipboard, + createAnyEnvironmentCommand, createEnvironmentCommand, createTerminalCommand, getPackageCommandOptions, - handlePackagesCommand, - refreshManagerCommand, + handlePackageUninstall, + refreshPackagesCommand, removeEnvironmentCommand, removePythonProject, + revealProjectInExplorer, runAsTaskCommand, + runInDedicatedTerminalCommand, runInTerminalCommand, - setEnvManagerCommand, setEnvironmentCommand, - setPkgManagerCommand, - resetEnvironmentCommand, - refreshPackagesCommand, - createAnyEnvironmentCommand, + setEnvManagerCommand, + setPackageManagerCommand, } from './features/envCommands'; -import { registerCondaFeatures } from './managers/conda/main'; -import { registerSystemPythonFeatures } from './managers/sysPython/main'; +import { PythonEnvironmentManagers } from './features/envManagers'; +import { EnvVarManager, PythonEnvVariableManager } from './features/execution/envVariableManager'; import { PythonProjectManagerImpl } from './features/projectManager'; -import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; import { getPythonApi, setPythonApi } from './features/pythonApi'; -import { setPersistentState } from './common/persistentState'; -import { isPythonProjectFile } from './common/utils/fileNameUtils'; -import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; -import { PythonEnvironmentApi } from './api'; -import { - ProjectCreatorsImpl, - registerAutoProjectProvider, - registerExistingProjectProvider, -} from './features/projectCreators'; -import { WorkspaceView } from './features/views/projectView'; import { registerCompletionProvider } from './features/settings/settingCompletions'; +import { setActivateMenuButtonContext } from './features/terminal/activateMenuButton'; +import { normalizeShellPath } from './features/terminal/shells/common/shellUtils'; +import { + clearShellProfileCache, + createShellEnvProviders, + createShellStartupProviders, +} from './features/terminal/shells/providers'; +import { ShellStartupActivationVariablesManagerImpl } from './features/terminal/shellStartupActivationVariablesManager'; +import { cleanupStartupScripts } from './features/terminal/shellStartupSetupHandlers'; +import { TerminalActivationImpl } from './features/terminal/terminalActivationState'; +import { TerminalEnvVarInjector } from './features/terminal/terminalEnvVarInjector'; +import { TerminalManager, TerminalManagerImpl } from './features/terminal/terminalManager'; +import { getEnvironmentForTerminal } from './features/terminal/utils'; +import { EnvManagerView } from './features/views/envManagersView'; +import { ProjectView } from './features/views/projectView'; +import { PythonStatusBarImpl } from './features/views/pythonStatusBar'; +import { updateViewsAndStatus } from './features/views/revealHandler'; +import { ProjectItem } from './features/views/treeViewItems'; +import { + collectEnvironmentInfo, + getEnvManagerAndPackageManagerConfigLevels, + resolveDefaultInterpreter, + runPetInTerminalImpl, +} from './helpers'; +import { EnvironmentManagers, ProjectCreators, PythonProjectManager } from './internal.api'; +import { registerSystemPythonFeatures } from './managers/builtin/main'; +import { SysPythonManager } from './managers/builtin/sysPythonManager'; +import { createNativePythonFinder, NativePythonFinder } from './managers/common/nativePythonFinder'; +import { IDisposable } from './managers/common/types'; +import { registerCondaFeatures } from './managers/conda/main'; +import { registerPipenvFeatures } from './managers/pipenv/main'; +import { registerPoetryFeatures } from './managers/poetry/main'; +import { registerPyenvFeatures } from './managers/pyenv/main'; + +export async function activate(context: ExtensionContext): Promise { + const useEnvironmentsExtension = getConfiguration('python').get('useEnvironmentsExtension', true); + traceInfo(`Experiment Status: useEnvironmentsExtension setting set to ${useEnvironmentsExtension}`); + if (!useEnvironmentsExtension) { + traceWarn( + 'The Python environments extension has been disabled via a setting. If you would like to opt into using the extension, please add the following to your user settings (note that updating this setting requires a window reload afterwards):\n\n"python.useEnvironmentsExtension": true', + ); + await deactivate(context); + return; + } + const start = new StopWatch(); -export async function activate(context: ExtensionContext): Promise { // Logging should be set up before anything else. - const outputChannel: LogOutputChannel = window.createOutputChannel('Python Environments', { log: true }); + const outputChannel: LogOutputChannel = createLogOutputChannel('Python Environments'); context.subscriptions.push(outputChannel, registerLogger(outputChannel)); + ensureCorrectVersion(); + + // log extension version + traceVerbose(`Python-envs extension version: ${extensionVersion}`); + // log settings + const configLevels = getEnvManagerAndPackageManagerConfigLevels(); + traceInfo(`\n=== ${configLevels.section} ===`); + traceInfo(JSON.stringify(configLevels, null, 2)); + // Setup the persistent state for the extension. setPersistentState(context); + const statusBar = new PythonStatusBarImpl(); + context.subscriptions.push(statusBar); + const projectManager: PythonProjectManager = new PythonProjectManagerImpl(); context.subscriptions.push(projectManager); + const envVarManager: EnvVarManager = new PythonEnvVariableManager(projectManager); + context.subscriptions.push(envVarManager); + const envManagers: EnvironmentManagers = new PythonEnvironmentManagers(projectManager); + createManagerReady(envManagers, projectManager, context.subscriptions); context.subscriptions.push(envManagers); + const terminalActivation = new TerminalActivationImpl(); + const shellEnvsProviders = createShellEnvProviders(); + const shellStartupProviders = createShellStartupProviders(); + + const terminalManager: TerminalManager = new TerminalManagerImpl( + terminalActivation, + shellEnvsProviders, + shellStartupProviders, + ); + context.subscriptions.push(terminalActivation, terminalManager); + const projectCreators: ProjectCreators = new ProjectCreatorsImpl(); context.subscriptions.push( projectCreators, - registerExistingProjectProvider(projectCreators), - registerAutoProjectProvider(projectCreators), + projectCreators.registerPythonProjectCreator(new ExistingProjects(projectManager)), + projectCreators.registerPythonProjectCreator(new AutoFindProjects(projectManager)), + projectCreators.registerPythonProjectCreator(new NewPackageProject(envManagers, projectManager)), + projectCreators.registerPythonProjectCreator(new NewScriptProject(projectManager)), ); - setPythonApi(envManagers, projectManager, projectCreators); - + setPythonApi(envManagers, projectManager, projectCreators, terminalManager, envVarManager); + const api = await getPythonApi(); + const sysPythonManager = createDeferred(); const managerView = new EnvManagerView(envManagers); context.subscriptions.push(managerView); - const workspaceView = new WorkspaceView(envManagers, projectManager); + const workspaceView = new ProjectView(envManagers, projectManager); context.subscriptions.push(workspaceView); - workspaceView.initialize(); - const api = await getPythonApi(); + + const monitoredTerminals = new Map(); + const shellStartupVarsMgr = new ShellStartupActivationVariablesManagerImpl( + context.environmentVariableCollection, + shellEnvsProviders, + api, + ); + + // Initialize terminal environment variable injection + const terminalEnvVarInjector = new TerminalEnvVarInjector(context.environmentVariableCollection, envVarManager); + context.subscriptions.push(terminalEnvVarInjector); context.subscriptions.push( + shellStartupVarsMgr, registerCompletionProvider(envManagers), - commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()), - commands.registerCommand('python-envs.refreshManager', async (item) => { - await refreshManagerCommand(item); + commands.registerCommand('python-envs.terminal.revertStartupScriptChanges', async () => { + await cleanupStartupScripts(shellStartupProviders); }), + commands.registerCommand('python-envs.viewLogs', () => outputChannel.show()), commands.registerCommand('python-envs.refreshAllManagers', async () => { await Promise.all(envManagers.managers.map((m) => m.refresh(undefined))); }), commands.registerCommand('python-envs.refreshPackages', async (item) => { - await refreshPackagesCommand(item); + await refreshPackagesCommand(item, envManagers); }), commands.registerCommand('python-envs.create', async (item) => { - await createEnvironmentCommand(item, envManagers, projectManager); + // Telemetry: record environment creation attempt with selected manager + let managerId = 'unknown'; + if (item && item.manager && item.manager.id) { + managerId = item.manager.id; + } + sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, { + manager: managerId, + triggeredLocation: 'createSpecifiedCommand', + }); + return await createEnvironmentCommand(item, envManagers, projectManager); }), - commands.registerCommand('python-envs.createAny', async () => { - await createAnyEnvironmentCommand(envManagers, projectManager); + commands.registerCommand('python-envs.createAny', async (options) => { + // Telemetry: record environment creation attempt with no specific manager + sendTelemetryEvent(EventNames.CREATE_ENVIRONMENT, undefined, { + manager: 'none', + triggeredLocation: 'createAnyCommand', + }); + return await createAnyEnvironmentCommand( + envManagers, + projectManager, + options ?? { selectEnvironment: true }, + ); }), commands.registerCommand('python-envs.remove', async (item) => { await removeEnvironmentCommand(item, envManagers); @@ -97,79 +210,211 @@ export async function activate(context: ExtensionContext): Promise { + await handlePackageUninstall(context, envManagers); }), commands.registerCommand('python-envs.set', async (item) => { - const result = await setEnvironmentCommand(item, envManagers, projectManager); - if (result) { - workspaceView.updateProject(result.workspace); - } + await setEnvironmentCommand(item, envManagers, projectManager); }), commands.registerCommand('python-envs.setEnv', async (item) => { - const result = await setEnvironmentCommand(item, envManagers, projectManager); - if (result) { - workspaceView.updateProject(result.workspace); - } - }), - commands.registerCommand('python-envs.reset', async (item) => { - await resetEnvironmentCommand(item, envManagers, projectManager); + await setEnvironmentCommand(item, envManagers, projectManager); }), commands.registerCommand('python-envs.setEnvManager', async () => { await setEnvManagerCommand(envManagers, projectManager); }), commands.registerCommand('python-envs.setPkgManager', async () => { - await setPkgManagerCommand(envManagers, projectManager); + await setPackageManagerCommand(envManagers, projectManager); + }), + commands.registerCommand('python-envs.addPythonProject', async () => { + await addPythonProjectCommand(undefined, projectManager, envManagers, projectCreators); + const totalProjectCount = projectManager.getProjects().length + 1; + sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, { + template: 'none', + quickCreate: false, + totalProjectCount, + triggeredLocation: 'add', + }); }), - commands.registerCommand('python-envs.addPythonProject', async (resource) => { - await addPythonProject(resource, projectManager, envManagers, projectCreators); + commands.registerCommand('python-envs.addPythonProjectGivenResource', async (resource) => { + await addPythonProjectCommand(resource, projectManager, envManagers, projectCreators); + const totalProjectCount = projectManager.getProjects().length + 1; + sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, { + template: 'none', + quickCreate: false, + totalProjectCount, + triggeredLocation: 'addGivenResource', + }); }), commands.registerCommand('python-envs.removePythonProject', async (item) => { - await resetEnvironmentCommand(item, envManagers, projectManager); + // Clear environment association before removing project + if (item instanceof ProjectItem) { + const uri = item.project.uri; + const manager = envManagers.getEnvironmentManager(uri); + if (manager) { + manager.set(uri, undefined); + } else { + traceError(`No environment manager found for ${uri.fsPath}`); + } + } await removePythonProject(item, projectManager); }), commands.registerCommand('python-envs.clearCache', async () => { + await clearPersistentState(); await envManagers.clearCache(undefined); + await clearShellProfileCache(shellStartupProviders); }), commands.registerCommand('python-envs.runInTerminal', (item) => { - return runInTerminalCommand(item, api); + return runInTerminalCommand(item, api, terminalManager); + }), + commands.registerCommand('python-envs.runInDedicatedTerminal', (item) => { + return runInDedicatedTerminalCommand(item, api, terminalManager); }), commands.registerCommand('python-envs.runAsTask', (item) => { return runAsTaskCommand(item, api); }), commands.registerCommand('python-envs.createTerminal', (item) => { - return createTerminalCommand(item, api); - }), - window.onDidChangeActiveTextEditor(async (e: TextEditor | undefined) => { - if (e && !e.document.isUntitled && e.document.uri.scheme === 'file') { - if ( - e.document.languageId === 'python' || - e.document.languageId === 'pip-requirements' || - isPythonProjectFile(e.document.uri.fsPath) - ) { - const env = await workspaceView.reveal(e.document.uri); - await managerView.reveal(env); + return createTerminalCommand(item, api, terminalManager); + }), + commands.registerCommand('python-envs.copyEnvPath', async (item) => { + await copyPathToClipboard(item); + }), + commands.registerCommand('python-envs.copyProjectPath', async (item) => { + await copyPathToClipboard(item); + }), + commands.registerCommand('python-envs.revealProjectInExplorer', async (item) => { + await revealProjectInExplorer(item); + }), + commands.registerCommand('python-envs.terminal.activate', async () => { + const terminal = activeTerminal(); + if (terminal) { + const env = await getEnvironmentForTerminal(api, terminal); + if (env) { + await terminalManager.activate(terminal, env); } } }), - envManagers.onDidChangeEnvironment(async (e) => { - const activeDocument = window.activeTextEditor?.document; - if (!activeDocument || activeDocument.isUntitled || activeDocument.uri.scheme !== 'file') { - return; + commands.registerCommand('python-envs.terminal.deactivate', async () => { + const terminal = activeTerminal(); + if (terminal) { + await terminalManager.deactivate(terminal); } + }), + commands.registerCommand( + 'python-envs.createNewProjectFromTemplate', + async (projectType: string, quickCreate: boolean, newProjectName: string, newProjectPath: string) => { + let projectTemplateName = projectType || 'unknown'; + let triggeredLocation: 'templateCreate' = 'templateCreate'; + let totalProjectCount = projectManager.getProjects().length + 1; + if (quickCreate) { + if (!projectType || !newProjectName || !newProjectPath) { + throw new Error('Project type, name, and path are required for quick create.'); + } + const creators = projectCreators.getProjectCreators(); + let selected: PythonProjectCreator | undefined; + if (projectType === 'python-package') { + selected = creators.find((c) => c.name === 'newPackage'); + } + if (projectType === 'python-script') { + selected = creators.find((c) => c.name === 'newScript'); + } + if (!selected) { + throw new Error(`Project creator for type "${projectType}" not found.`); + } + await selected.create({ + quickCreate: true, + name: newProjectName, + rootUri: Uri.file(newProjectPath), + }); + } else { + const selected = await newProjectSelection(projectCreators.getProjectCreators()); + if (selected) { + projectTemplateName = selected.name || 'unknown'; + await selected.create(); + } + } + sendTelemetryEvent(EventNames.ADD_PROJECT, undefined, { + template: projectTemplateName, + quickCreate: quickCreate, + totalProjectCount, + triggeredLocation, + }); + }, + ), + commands.registerCommand('python-envs.reportIssue', async () => { + try { + const issueData = await collectEnvironmentInfo(context, envManagers, projectManager); - if ( - activeDocument.languageId !== 'python' && - activeDocument.languageId !== 'pip-requirements' && - !isPythonProjectFile(activeDocument.uri.fsPath) - ) { + await commands.executeCommand('workbench.action.openIssueReporter', { + extensionId: 'ms-python.vscode-python-envs', + issueTitle: '[Python Environments] ', + issueBody: `\n\n\n\n

\nEnvironment Information\n\n\`\`\`\n${issueData}\n\`\`\`\n\n
`, + }); + } catch (error) { + window.showErrorMessage(`Failed to open issue reporter: ${error}`); + } + }), + commands.registerCommand('python-envs.runPetInTerminal', async () => { + try { + await runPetInTerminalImpl(); + } catch (error) { + traceError('Error running PET in terminal', error); + window.showErrorMessage(`Failed to run Python Environment Tool: ${error}`); + } + }), + terminalActivation.onDidChangeTerminalActivationState(async (e) => { + await setActivateMenuButtonContext(e.terminal, e.environment, e.activated); + }), + onDidChangeActiveTerminal(async (t) => { + if (t) { + const env = terminalActivation.getEnvironment(t) ?? (await getEnvironmentForTerminal(api, t)); + if (env) { + await setActivateMenuButtonContext(t, env, terminalActivation.isActivated(t)); + } + } + }), + window.onDidChangeActiveTextEditor(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); + }), + envManagers.onDidChangeEnvironment(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); + }), + envManagers.onDidChangeEnvironments(async () => { + updateViewsAndStatus(statusBar, workspaceView, managerView, api); + }), + envManagers.onDidChangeEnvironmentFiltered(async (e) => { + managerView.environmentChanged(e); + const location = e.uri?.fsPath ?? 'global'; + traceInfo( + `Internal: Changed environment from ${e.old?.displayName} to ${e.new?.displayName} for: ${location}`, + ); + updateViewsAndStatus(statusBar, workspaceView, managerView, api); + }), + onDidChangeTerminalShellIntegration(async (e) => { + const shellEnv = e.shellIntegration?.env; + if (!shellEnv) { return; } - - const mgr1 = envManagers.getEnvironmentManager(e.uri); - const mgr2 = envManagers.getEnvironmentManager(activeDocument.uri); - if (mgr1 === mgr2 && e.new) { - const env = await workspaceView.reveal(activeDocument.uri); - await managerView.reveal(env); + const envVar = shellEnv.value; + if (envVar) { + if (envVar['VIRTUAL_ENV']) { + const envPath = normalizeShellPath(envVar['VIRTUAL_ENV'], e.terminal.state.shell); + const env = await api.resolveEnvironment(Uri.file(envPath)); + if (env) { + monitoredTerminals.set(e.terminal, env); + terminalActivation.updateActivationState(e.terminal, env, true); + } + } else if (monitoredTerminals.has(e.terminal)) { + const env = monitoredTerminals.get(e.terminal); + if (env) { + terminalActivation.updateActivationState(e.terminal, env, false); + } + } } }), ); @@ -177,19 +422,48 @@ export async function activate(context: ExtensionContext): Promise { + // This is the finder that is used by all the built in environment managers + const nativeFinder: NativePythonFinder = await createNativePythonFinder(outputChannel, api, context); + context.subscriptions.push(nativeFinder); + const sysMgr = new SysPythonManager(nativeFinder, api, outputChannel); + sysPythonManager.resolve(sysMgr); await Promise.all([ - registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel), - registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel), + registerSystemPythonFeatures(nativeFinder, context.subscriptions, outputChannel, sysMgr), + registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), + registerPyenvFeatures(nativeFinder, context.subscriptions, projectManager), + registerPipenvFeatures(nativeFinder, context.subscriptions, projectManager), + registerPoetryFeatures(nativeFinder, context.subscriptions, outputChannel, projectManager), + shellStartupVarsMgr.initialize(), ]); + + await resolveDefaultInterpreter(nativeFinder, envManagers, api); + + sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime); + await terminalManager.initialize(api); + sendManagerSelectionTelemetry(projectManager); }); + sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime); + return api; } -export function deactivate() {} +export async function disposeAll(disposables: IDisposable[]): Promise { + await Promise.all( + disposables.map(async (d) => { + try { + return Promise.resolve(d.dispose()); + } catch (_err) { + // do nothing + } + return Promise.resolve(); + }), + ); +} + +export async function deactivate(context: ExtensionContext) { + await disposeAll(context.subscriptions); + context.subscriptions.length = 0; // Clear subscriptions to prevent memory leaks + traceInfo('Python Environments extension deactivated.'); +} diff --git a/src/features/common/activation.ts b/src/features/common/activation.ts new file mode 100644 index 0000000..f4b2067 --- /dev/null +++ b/src/features/common/activation.ts @@ -0,0 +1,34 @@ +import { Terminal } from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { + getShellActivationCommand, + getShellCommandAsString, + getShellDeactivationCommand, +} from '../terminal/shells/common/shellUtils'; +import { identifyTerminalShell } from './shellDetector'; + +export function isActivatableEnvironment(environment: PythonEnvironment): boolean { + return !!environment.execInfo?.activation || !!environment.execInfo?.shellActivation; +} + +export function isActivatedRunAvailable(environment: PythonEnvironment): boolean { + return !!environment.execInfo?.activatedRun; +} + +export function getActivationCommand(terminal: Terminal, environment: PythonEnvironment): string | undefined { + const shell = identifyTerminalShell(terminal); + const command = getShellActivationCommand(shell, environment); + if (command) { + return getShellCommandAsString(shell, command); + } + return undefined; +} + +export function getDeactivationCommand(terminal: Terminal, environment: PythonEnvironment): string | undefined { + const shell = identifyTerminalShell(terminal); + const command = getShellDeactivationCommand(shell, environment); + if (command) { + return getShellCommandAsString(shell, command); + } + return undefined; +} diff --git a/src/features/common/managerReady.ts b/src/features/common/managerReady.ts new file mode 100644 index 0000000..a7341b8 --- /dev/null +++ b/src/features/common/managerReady.ts @@ -0,0 +1,228 @@ +import { Disposable, l10n, Uri } from 'vscode'; +import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { allExtensions, getExtension } from '../../common/extension.apis'; +import { traceError, traceInfo } from '../../common/logging'; +import { showErrorMessage } from '../../common/window.apis'; +import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from '../settings/settingHelpers'; +import { WorkbenchStrings } from '../../common/localize'; +import { installExtension } from '../../common/workbenchCommands'; + +interface ManagerReady extends Disposable { + waitForEnvManager(uris?: Uri[]): Promise; + waitForEnvManagerId(managerIds: string[]): Promise; + waitForAllEnvManagers(): Promise; + waitForPkgManager(uris?: Uri[]): Promise; + waitForPkgManagerId(managerIds: string[]): Promise; +} + +function getExtensionId(managerId: string): string | undefined { + // format : + const regex = /^(.*):([a-zA-Z0-9-_]*)$/; + const parts = regex.exec(managerId); + return parts ? parts[1] : undefined; +} + +class ManagerReadyImpl implements ManagerReady { + private readonly envManagers: Map> = new Map(); + private readonly pkgManagers: Map> = new Map(); + private readonly checked: Set = new Set(); + private readonly disposables: Disposable[] = []; + + constructor(em: EnvironmentManagers, private readonly pm: PythonProjectManager) { + this.disposables.push( + em.onDidChangeEnvironmentManager((e) => { + if (this.envManagers.has(e.manager.id)) { + this.envManagers.get(e.manager.id)?.resolve(); + } else { + const deferred = createDeferred(); + this.envManagers.set(e.manager.id, deferred); + deferred.resolve(); + } + }), + em.onDidChangePackageManager((e) => { + if (this.pkgManagers.has(e.manager.id)) { + this.pkgManagers.get(e.manager.id)?.resolve(); + } else { + const deferred = createDeferred(); + this.pkgManagers.set(e.manager.id, deferred); + deferred.resolve(); + } + }), + ); + } + + private checkExtension(managerId: string) { + const installed = allExtensions().some((ext) => managerId.startsWith(`${ext.id}:`)); + if (this.checked.has(managerId)) { + return; + } + this.checked.add(managerId); + const extId = getExtensionId(managerId); + if (extId) { + setImmediate(async () => { + if (installed) { + const ext = getExtension(extId); + if (ext && !ext.isActive) { + traceInfo(`Extension for manager ${managerId} is not active: Activating...`); + try { + await ext.activate(); + traceInfo(`Extension for manager ${managerId} is now active.`); + } catch (err) { + traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err); + } + } + } else { + traceError(`Extension for manager ${managerId} is not installed.`); + const result = await showErrorMessage( + l10n.t(`Do you want to install extension {0} to enable {1} support.`, extId, managerId), + WorkbenchStrings.installExtension, + ); + if (result === WorkbenchStrings.installExtension) { + traceInfo(`Installing extension: ${extId}`); + try { + await installExtension(extId); + traceInfo(`Extension ${extId} installed.`); + } catch (err) { + traceError(`Failed to install extension: ${extId}`, err); + } + + try { + const ext = getExtension(extId); + if (ext && !ext.isActive) { + traceInfo(`Extension for manager ${managerId} is not active: Activating...`); + await ext.activate(); + } + } catch (err) { + traceError(`Failed to activate extension ${extId}, required for: ${managerId}`, err); + } + } + } + }); + } else { + showErrorMessage(l10n.t(`Extension for {0} is not installed or enabled for this workspace.`, managerId)); + } + } + + public dispose(): void { + this.disposables.forEach((d) => d.dispose()); + this.envManagers.clear(); + this.pkgManagers.clear(); + } + + private _waitForEnvManager(managerId: string): Promise { + if (this.envManagers.has(managerId)) { + return this.envManagers.get(managerId)!.promise; + } + const deferred = createDeferred(); + this.envManagers.set(managerId, deferred); + return deferred.promise; + } + + public async waitForEnvManager(uris?: Uri[]): Promise { + const ids: Set = new Set(); + if (uris) { + uris.forEach((uri) => { + const m = getDefaultEnvManagerSetting(this.pm, uri); + if (!ids.has(m)) { + ids.add(m); + } + }); + } else { + const m = getDefaultEnvManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + } + + await this.waitForEnvManagerId(Array.from(ids)); + } + + public async waitForEnvManagerId(managerIds: string[]): Promise { + managerIds.forEach((managerId) => this.checkExtension(managerId)); + await Promise.all(managerIds.map((managerId) => this._waitForEnvManager(managerId))); + } + + public async waitForAllEnvManagers(): Promise { + const ids: Set = new Set(); + this.pm.getProjects().forEach((project) => { + const m = getDefaultEnvManagerSetting(this.pm, project.uri); + if (m && !ids.has(m)) { + ids.add(m); + } + }); + + const m = getDefaultEnvManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + await this.waitForEnvManagerId(Array.from(ids)); + } + + private _waitForPkgManager(managerId: string): Promise { + if (this.pkgManagers.has(managerId)) { + return this.pkgManagers.get(managerId)!.promise; + } + const deferred = createDeferred(); + this.pkgManagers.set(managerId, deferred); + return deferred.promise; + } + + public async waitForPkgManager(uris?: Uri[]): Promise { + const ids: Set = new Set(); + + if (uris) { + uris.forEach((uri) => { + const m = getDefaultPkgManagerSetting(this.pm, uri); + if (!ids.has(m)) { + ids.add(m); + } + }); + } else { + const m = getDefaultPkgManagerSetting(this.pm, undefined); + if (m) { + ids.add(m); + } + } + + await this.waitForPkgManagerId(Array.from(ids)); + } + public async waitForPkgManagerId(managerIds: string[]): Promise { + managerIds.forEach((managerId) => this.checkExtension(managerId)); + await Promise.all(managerIds.map((managerId) => this._waitForPkgManager(managerId))); + } +} + +let _deferred = createDeferred(); +export function createManagerReady(em: EnvironmentManagers, pm: PythonProjectManager, disposables: Disposable[]) { + if (!_deferred.completed) { + const mr = new ManagerReadyImpl(em, pm); + disposables.push(mr); + _deferred.resolve(mr); + } +} + +export async function waitForEnvManager(uris?: Uri[]): Promise { + const mr = await _deferred.promise; + return mr.waitForEnvManager(uris); +} + +export async function waitForEnvManagerId(managerIds: string[]): Promise { + const mr = await _deferred.promise; + return mr.waitForEnvManagerId(managerIds); +} + +export async function waitForAllEnvManagers(): Promise { + const mr = await _deferred.promise; + return mr.waitForAllEnvManagers(); +} + +export async function waitForPkgManager(uris?: Uri[]): Promise { + const mr = await _deferred.promise; + return mr.waitForPkgManager(uris); +} + +export async function waitForPkgManagerId(managerIds: string[]): Promise { + const mr = await _deferred.promise; + return mr.waitForPkgManagerId(managerIds); +} diff --git a/src/features/common/shellConstants.ts b/src/features/common/shellConstants.ts new file mode 100644 index 0000000..7442097 --- /dev/null +++ b/src/features/common/shellConstants.ts @@ -0,0 +1,15 @@ +export namespace ShellConstants { + export const PWSH = 'pwsh'; + export const BASH = 'bash'; + export const ZSH = 'zsh'; + export const FISH = 'fish'; + export const CMD = 'cmd'; + export const SH = 'sh'; + export const NU = 'nu'; + export const GITBASH = 'gitbash'; + export const WSL = 'wsl'; + export const CSH = 'csh'; + export const TCSH = 'tcsh'; + export const KSH = 'ksh'; + export const XONSH = 'xonsh'; +} diff --git a/src/features/execution/shellDetector.ts b/src/features/common/shellDetector.ts similarity index 55% rename from src/features/execution/shellDetector.ts rename to src/features/common/shellDetector.ts index 5d5acb4..4627e86 100644 --- a/src/features/execution/shellDetector.ts +++ b/src/features/common/shellDetector.ts @@ -1,9 +1,9 @@ -import { Terminal } from 'vscode'; -import { isWindows } from '../../managers/common/utils'; import * as os from 'os'; +import { Terminal } from 'vscode'; import { vscodeShell } from '../../common/vscodeEnv.apis'; import { getConfiguration } from '../../common/workspace.apis'; -import { TerminalShellType } from '../../api'; +import { isWindows } from '../../common/utils/platformUtils'; +import { ShellConstants } from './shellConstants'; /* When identifying the shell use the following algorithm: @@ -16,58 +16,62 @@ When identifying the shell use the following algorithm: // Types of shells can be found here: // 1. https://wiki.ubuntu.com/ChangingShells -const IS_GITBASH = /(gitbash$)/i; +const IS_GITBASH = /(gitbash$|git.bin.bash$|git-bash$)/i; const IS_BASH = /(bash$)/i; const IS_WSL = /(wsl$)/i; const IS_ZSH = /(zsh$)/i; const IS_KSH = /(ksh$)/i; const IS_COMMAND = /(cmd$)/i; -const IS_POWERSHELL = /(powershell$)/i; -const IS_POWERSHELL_CORE = /(pwsh$)/i; +const IS_POWERSHELL = /(powershell$|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(); -detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); -detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); -detectableShells.set(TerminalShellType.bash, IS_BASH); -detectableShells.set(TerminalShellType.wsl, IS_WSL); -detectableShells.set(TerminalShellType.zsh, IS_ZSH); -detectableShells.set(TerminalShellType.ksh, IS_KSH); -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); - -function identifyShellFromShellPath(shellPath: string): TerminalShellType { +const detectableShells = new Map([ + [ShellConstants.PWSH, IS_POWERSHELL], + [ShellConstants.GITBASH, IS_GITBASH], + [ShellConstants.BASH, IS_BASH], + [ShellConstants.WSL, IS_WSL], + [ShellConstants.ZSH, IS_ZSH], + [ShellConstants.KSH, IS_KSH], + [ShellConstants.CMD, IS_COMMAND], + [ShellConstants.FISH, IS_FISH], + [ShellConstants.TCSH, IS_TCSHELL], + [ShellConstants.CSH, IS_CSHELL], + [ShellConstants.NU, IS_NUSHELL], + [ShellConstants.XONSH, IS_XONSH], +]); + +function identifyShellFromShellPath(shellPath: string): string { // Remove .exe extension so shells can be more consistently detected // on Windows (including Cygwin). const basePath = shellPath.replace(/\.exe$/i, ''); const shell = Array.from(detectableShells.keys()).reduce((matchedShell, shellToDetect) => { - if (matchedShell === TerminalShellType.unknown) { + if (matchedShell === 'unknown') { const pat = detectableShells.get(shellToDetect); if (pat && pat.test(basePath)) { return shellToDetect; } } return matchedShell; - }, TerminalShellType.unknown); + }, 'unknown'); return shell; } -function identifyShellFromTerminalName(terminal: Terminal): TerminalShellType { +function identifyShellFromTerminalName(terminal: Terminal): string { + if (terminal.name === ShellConstants.SH) { + // Specifically checking this because other shells have `sh` at the end of their name + // We can match and return bash for this case + return ShellConstants.BASH; + } return identifyShellFromShellPath(terminal.name); } -function identifyPlatformDefaultShell(): TerminalShellType { +function identifyPlatformDefaultShell(): string { if (isWindows()) { return identifyShellFromShellPath(getTerminalDefaultShellWindows()); } @@ -84,16 +88,16 @@ function getTerminalDefaultShellWindows(): string { return isAtLeastWindows10 ? powerShellPath : process.env.comspec || 'cmd.exe'; } -function identifyShellFromVSC(terminal: Terminal): TerminalShellType { +function identifyShellFromVSC(terminal: Terminal): string { const shellPath = terminal?.creationOptions && 'shellPath' in terminal.creationOptions && terminal.creationOptions.shellPath ? terminal.creationOptions.shellPath : vscodeShell(); - return shellPath ? identifyShellFromShellPath(shellPath) : TerminalShellType.unknown; + return shellPath ? identifyShellFromShellPath(shellPath) : 'unknown'; } -function identifyShellFromSettings(): TerminalShellType { +function identifyShellFromSettings(): string { const shellConfig = getConfiguration('terminal.integrated.shell'); let shellPath: string | undefined; switch (process.platform) { @@ -115,21 +119,52 @@ function identifyShellFromSettings(): TerminalShellType { shellPath = undefined; } } - return shellPath ? identifyShellFromShellPath(shellPath) : TerminalShellType.unknown; + return shellPath ? identifyShellFromShellPath(shellPath) : 'unknown'; } -export function identifyTerminalShell(terminal: Terminal): TerminalShellType { - let shellType = identifyShellFromTerminalName(terminal); - - if (shellType === TerminalShellType.unknown) { - shellType = identifyShellFromSettings(); +function fromShellTypeApi(terminal: Terminal): string { + try { + const known = [ + ShellConstants.BASH, + ShellConstants.CMD, + ShellConstants.CSH, + ShellConstants.FISH, + ShellConstants.GITBASH, + 'julia', + ShellConstants.KSH, + 'node', + ShellConstants.NU, + ShellConstants.PWSH, + 'python', + ShellConstants.SH, + 'wsl', + ShellConstants.ZSH, + ]; + if (terminal.state.shell && known.includes(terminal.state.shell.toLowerCase())) { + return terminal.state.shell.toLowerCase(); + } + } catch { + // If the API is not available, return unknown } + return 'unknown'; +} - if (shellType === TerminalShellType.unknown) { +export function identifyTerminalShell(terminal: Terminal): string { + let shellType = fromShellTypeApi(terminal); + + if (shellType === 'unknown') { shellType = identifyShellFromVSC(terminal); } - if (shellType === TerminalShellType.unknown) { + if (shellType === 'unknown') { + shellType = identifyShellFromTerminalName(terminal); + } + + if (shellType === 'unknown') { + shellType = identifyShellFromSettings(); + } + + if (shellType === 'unknown') { shellType = identifyPlatformDefaultShell(); } diff --git a/src/features/creators/autoFindProjects.ts b/src/features/creators/autoFindProjects.ts new file mode 100644 index 0000000..7d3987c --- /dev/null +++ b/src/features/creators/autoFindProjects.ts @@ -0,0 +1,116 @@ +import * as path from 'path'; +import { Uri } from 'vscode'; +import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api'; +import { ProjectCreatorString } from '../../common/localize'; +import { traceInfo } from '../../common/logging'; +import { showErrorMessage, showQuickPickWithButtons, showWarningMessage } from '../../common/window.apis'; +import { findFiles } from '../../common/workspace.apis'; +import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api'; + +function getUniqueUri(uris: Uri[]): { + label: string; + description: string; + uri: Uri; +}[] { + const files = uris.map((uri) => uri.fsPath).sort(); + const dirs: Map = new Map(); + files.forEach((file) => { + const dir = path.dirname(file); + if (dirs.has(dir)) { + return; + } + dirs.set(dir, file); + }); + return Array.from(dirs.entries()) + .map(([dir, file]) => ({ + label: path.basename(dir), + description: file, + uri: Uri.file(dir), + })) + .sort((a, b) => a.label.localeCompare(b.label)); +} + +async function pickProjects(uris: Uri[]): Promise { + const items = getUniqueUri(uris); + + const selected = await showQuickPickWithButtons(items, { + canPickMany: true, + ignoreFocusOut: true, + placeHolder: ProjectCreatorString.selectProjects, + showBackButton: true, + }); + + if (Array.isArray(selected)) { + return selected.map((s) => s.uri); + } else if (selected) { + return [selected.uri]; + } + + return undefined; +} + +export class AutoFindProjects implements PythonProjectCreator { + public readonly name = 'autoProjects'; + public readonly displayName = ProjectCreatorString.autoFindProjects; + public readonly description = ProjectCreatorString.autoFindProjectsDescription; + + supportsQuickCreate = true; + + constructor(private readonly pm: PythonProjectManager) {} + + async create(_options?: PythonProjectCreatorOptions): Promise { + const files = await findFiles('**/{pyproject.toml,setup.py}', '**/.venv/**'); + if (!files || files.length === 0) { + setImmediate(() => { + showErrorMessage('No projects found'); + }); + return; + } + + const filtered = files.filter((uri) => { + const p = this.pm.get(uri); + if (p) { + // Skip this project if: + // 1. There's already a project registered with exactly the same path + // 2. There's already a project registered with this project's parent directory path + const np = path.normalize(p.uri.fsPath); + const nf = path.normalize(uri.fsPath); + const nfp = path.dirname(nf); + return np !== nf && np !== nfp; + } + return true; + }); + + if (filtered.length === 0) { + // No new projects found that are not already in the project manager + traceInfo( + `All selected resources are already registered in the project manager: ${files + .map((uri) => uri.fsPath) + .join(', ')}`, + ); + setImmediate(() => { + if (files.length === 1) { + showWarningMessage(`${files[0].fsPath} already exists as project.`); + } else { + showWarningMessage('Selected resources already exist as projects.'); + } + }); + return; + } + + traceInfo(`Found ${filtered.length} new potential projects that aren't already registered`); + + const projectUris = await pickProjects(filtered); + if (!projectUris || projectUris.length === 0) { + // User cancelled the selection. + traceInfo('User cancelled project selection.'); + return; + } + const projects = projectUris.map( + (uri) => new PythonProjectsImpl(path.basename(uri.fsPath), uri), + ) as PythonProject[]; + // Add the projects to the project manager + this.pm.add(projects); + return projects; + } +} diff --git a/src/features/creators/creationHelpers.ts b/src/features/creators/creationHelpers.ts new file mode 100644 index 0000000..066423c --- /dev/null +++ b/src/features/creators/creationHelpers.ts @@ -0,0 +1,203 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { extensions, l10n, QuickInputButtons, Uri, window } from 'vscode'; +import { CreateEnvironmentOptions } from '../../api'; +import { traceError, traceVerbose } from '../../common/logging'; +import { showQuickPickWithButtons } from '../../common/window.apis'; +import { EnvironmentManagers, InternalEnvironmentManager } from '../../internal.api'; + +/** + * Prompts the user to choose whether to create a new virtual environment (venv) for a project, with a clearer return and early exit. + * @returns {Promise} Resolves to true if 'Yes' is selected, false if 'No', or undefined if cancelled. + */ +export async function promptForVenv(callback: () => void): Promise { + try { + const venvChoice = await showQuickPickWithButtons([{ label: l10n.t('Yes') }, { label: l10n.t('No') }], { + placeHolder: l10n.t('Would you like to create a new virtual environment for this project?'), + ignoreFocusOut: true, + showBackButton: true, + }); + if (!venvChoice) { + return undefined; + } + if (Array.isArray(venvChoice)) { + // Should not happen for single selection, but handle just in case + return venvChoice.some((item) => item.label === 'Yes'); + } + return venvChoice.label === 'Yes'; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + callback(); + } + } +} + +/** + * Checks if the GitHub Copilot extension is installed in the current VS Code environment. + * @returns {boolean} True if Copilot is installed, false otherwise. + */ +export function isCopilotInstalled(): boolean { + return !!extensions.getExtension('GitHub.copilot'); +} + +/** + * Prompts the user to choose whether to create a Copilot instructions file, only if Copilot is installed. + * @returns {Promise} Resolves to true if 'Yes' is selected, false if 'No', or undefined if cancelled or Copilot is not installed. + */ +export async function promptForCopilotInstructions(): Promise { + if (!isCopilotInstalled()) { + return undefined; + } + const copilotChoice = await showQuickPickWithButtons([{ label: 'Yes' }, { label: 'No' }], { + placeHolder: 'Would you like to create a Copilot instructions file?', + ignoreFocusOut: true, + showBackButton: true, + }); + if (!copilotChoice) { + return undefined; + } + if (Array.isArray(copilotChoice)) { + // Should not happen for single selection, but handle just in case + return copilotChoice.some((item) => item.label === 'Yes'); + } + return copilotChoice.label === 'Yes'; +} + +/** + * Quickly creates a new Python virtual environment (venv) in the specified destination folder using the available environment managers. + * Attempts to use the venv manager if available, otherwise falls back to any manager that supports environment creation. + * @param envManagers - The collection of available environment managers. + * @param destFolder - The absolute path to the destination folder where the environment should be created. + * @returns {Promise} Resolves when the environment is created or an error is shown. + */ +export async function quickCreateNewVenv(envManagers: EnvironmentManagers, destFolder: string) { + // get the environment manager for venv, should always exist + const envManager: InternalEnvironmentManager | undefined = envManagers.managers.find( + (m) => m.id === 'ms-python.python:venv', + ); + const destinationUri = Uri.parse(destFolder); + if (envManager?.supportsQuickCreate) { + // with quickCreate enabled, user will not be prompted when creating the environment + const options: CreateEnvironmentOptions = { quickCreate: false }; + if (envManager.supportsQuickCreate) { + options.quickCreate = true; + } + const pyEnv = await envManager.create(destinationUri, options); + // TODO: do I need to update to say this is the env for the file? Like set it? + if (!pyEnv) { + // comes back as undefined if this doesn't work + window.showErrorMessage(`Failed to create virtual environment, please create it manually.`); + } else { + traceVerbose(`Created venv at: ${pyEnv?.environmentPath}`); + } + } else { + window.showErrorMessage(`Failed to quick create virtual environment, please create it manually.`); + } +} + +/** + * Replaces all occurrences of a string in file and folder names, as well as file contents, within a directory tree or a single file. + * @param targetPath - The root directory or file path to start the replacement from. + * @param searchValue - The string to search for in names and contents. + * @param replaceValue - The string to replace with. + * @returns {Promise} Resolves when all replacements are complete. + */ +export async function replaceInFilesAndNames(targetPath: string, searchValue: string, replaceValue: string) { + const stat = await fs.stat(targetPath); + + if (stat.isDirectory()) { + const entries = await fs.readdir(targetPath, { withFileTypes: true }); + for (const entry of entries) { + let entryName = entry.name; + let fullPath = path.join(targetPath, entryName); + let newFullPath = fullPath; + // If the file or folder name contains searchValue, rename it + if (entryName.includes(searchValue)) { + const newName = entryName.replace(new RegExp(searchValue, 'g'), replaceValue); + newFullPath = path.join(targetPath, newName); + await fs.rename(fullPath, newFullPath); + entryName = newName; + } + await replaceInFilesAndNames(newFullPath, searchValue, replaceValue); + } + } else if (stat.isFile()) { + let content = await fs.readFile(targetPath, 'utf8'); + if (content.includes(searchValue)) { + content = content.replace(new RegExp(searchValue, 'g'), replaceValue); + await fs.writeFile(targetPath, content, 'utf8'); + } + } +} + +/** + * Ensures the .github/copilot-instructions.md file exists at the given root, creating or appending as needed. + * Performs multiple find-and-replace operations as specified by the replacements array. + * @param destinationRootPath - The root directory where the .github folder should exist. + * @param instructionsFilePath - The path to the instructions template file. + * @param replacements - An array of tuples [{ text_to_find_and_replace, text_to_replace_it_with }] + */ +export async function manageCopilotInstructionsFile( + destinationRootPath: string, + instructionsFilePath: string, + replacements: Array<{ searchValue: string; replaceValue: string }>, +) { + let instructionsText = `\n\n` + (await fs.readFile(instructionsFilePath, 'utf-8')); + for (const { searchValue: text_to_find_and_replace, replaceValue: text_to_replace_it_with } of replacements) { + instructionsText = instructionsText.replace(new RegExp(text_to_find_and_replace, 'g'), text_to_replace_it_with); + } + const githubFolderPath = path.join(destinationRootPath, '.github'); + const customInstructionsPath = path.join(githubFolderPath, 'copilot-instructions.md'); + if (!(await fs.pathExists(githubFolderPath))) { + await fs.mkdir(githubFolderPath); + } + const customInstructions = await fs.pathExists(customInstructionsPath); + if (customInstructions) { + await fs.appendFile(customInstructionsPath, instructionsText); + } else { + await fs.writeFile(customInstructionsPath, instructionsText); + } +} + +/** + * Appends a configuration object to the configurations array in a launch.json file. + * @param launchJsonPath - The absolute path to the launch.json file. + * @param projectLaunchConfig - The stringified JSON config to append. + */ +async function appendToJsonConfigs(launchJsonPath: string, projectLaunchConfig: string) { + let content = await fs.readFile(launchJsonPath, 'utf8'); + const json = JSON.parse(content); + // If it's a VS Code launch config, append to configurations array + if (json && Array.isArray(json.configurations)) { + const configObj = JSON.parse(projectLaunchConfig); + json.configurations.push(configObj); + await fs.writeFile(launchJsonPath, JSON.stringify(json, null, 4), 'utf8'); + } else { + traceError('Failed to add Project Launch Config to launch.json.'); + return; + } +} + +/** + * Updates the launch.json file in the .vscode folder to include the provided project launch configuration. + * @param destinationRootPath - The root directory where the .vscode folder should exist. + * @param projectLaunchConfig - The stringified JSON config to append. + */ +export async function manageLaunchJsonFile(destinationRootPath: string, projectLaunchConfig: string) { + const vscodeFolderPath = path.join(destinationRootPath, '.vscode'); + const launchJsonPath = path.join(vscodeFolderPath, 'launch.json'); + if (!(await fs.pathExists(vscodeFolderPath))) { + await fs.mkdir(vscodeFolderPath); + } + const launchJsonExists = await fs.pathExists(launchJsonPath); + if (launchJsonExists) { + // Try to parse and append to existing launch.json + await appendToJsonConfigs(launchJsonPath, projectLaunchConfig); + } else { + // Create a new launch.json with the provided config + const launchJson = { + version: '0.2.0', + configurations: [JSON.parse(projectLaunchConfig)], + }; + await fs.writeFile(launchJsonPath, JSON.stringify(launchJson, null, 4), 'utf8'); + } +} diff --git a/src/features/creators/existingProjects.ts b/src/features/creators/existingProjects.ts new file mode 100644 index 0000000..5c66d29 --- /dev/null +++ b/src/features/creators/existingProjects.ts @@ -0,0 +1,118 @@ +import * as path from 'path'; +import { Uri, window, workspace } from 'vscode'; +import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api'; +import { ProjectCreatorString } from '../../common/localize'; +import { traceInfo, traceLog } from '../../common/logging'; +import { showOpenDialog, showWarningMessage } from '../../common/window.apis'; +import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api'; + +export class ExistingProjects implements PythonProjectCreator { + public readonly name = 'existingProjects'; + public readonly displayName = ProjectCreatorString.addExistingProjects; + + constructor(private readonly pm: PythonProjectManager) {} + + async create( + _options?: PythonProjectCreatorOptions, + ): Promise { + let existingAddUri: Uri[] | undefined; + if (_options?.rootUri) { + // If rootUri is provided, do not prompt + existingAddUri = [_options.rootUri]; + } else if (_options?.quickCreate) { + // If quickCreate is true & no rootUri is provided, we should not prompt for any input + throw new Error('Root URI is required in quickCreate mode.'); + } else { + // Prompt the user to select files or folders + existingAddUri = await showOpenDialog({ + canSelectFiles: true, + canSelectFolders: true, + canSelectMany: true, + filters: { + python: ['py'], + }, + title: ProjectCreatorString.selectFilesOrFolders, + }); + } + + if (!existingAddUri || existingAddUri.length === 0) { + // User cancelled the dialog & doesn't want to add any projects + return; + } + + // do we have any limitations that need to be applied here? + // like selected folder not child of workspace folder? + + const filtered = existingAddUri.filter((uri) => { + const p = this.pm.get(uri); + if (p) { + // Skip this project if there's already a project registered with exactly the same path + const np = path.normalize(p.uri.fsPath); + const nf = path.normalize(uri.fsPath); + return np !== nf; + } + return true; + }); + + if (filtered.length === 0) { + // No new projects found that are not already in the project manager + const formattedProjectPaths = + existingAddUri === undefined ? 'None' : existingAddUri.map((uri) => uri.fsPath).join(', '); + traceInfo( + `All selected resources are already registered in the project manager. Resources selected: ${formattedProjectPaths}`, + ); + setImmediate(() => { + if (existingAddUri && existingAddUri.length === 1) { + showWarningMessage(`Selected resource already exists as project.`); + } else { + showWarningMessage('Selected resources already exist as projects.'); + } + }); + return; + } + + // for all the selected files / folders, check to make sure they are in the workspace + const resultsOutsideWorkspace: Uri[] = []; + const workspaceRoots: Uri[] = workspace.workspaceFolders?.map((w) => w.uri) || []; + const resultsInWorkspace = filtered.filter((r) => { + const exists = workspaceRoots.some((w) => r.fsPath.startsWith(w.fsPath)); + if (!exists) { + traceLog(`File ${r.fsPath} is not in the workspace, ignoring it from 'add projects' list.`); + resultsOutsideWorkspace.push(r); + } + return exists; + }); + if (resultsInWorkspace.length === 0) { + // Show a single error message with option to add to workspace + const response = await window.showErrorMessage( + 'Selected items are not in the current workspace.', + 'Add to Workspace', + 'Cancel', + ); + + if (response === 'Add to Workspace') { + // Use the command palette to let user adjust which folders to add + // Add folders programmatically using workspace API + for (const r of resultsOutsideWorkspace) { + // if the user selects a file, add that file to the workspace + await // if the user selects a folder, add that folder to the workspace + await workspace.updateWorkspaceFolders( + workspace.workspaceFolders?.length || 0, // Start index + 0, // Delete count + { + uri: r, + }, + ); + } + } + return; + } else { + const projects = resultsInWorkspace.map( + (uri) => new PythonProjectsImpl(path.basename(uri.fsPath), uri), + ) as PythonProject[]; + // Add the projects to the project manager + this.pm.add(projects); + return projects; + } + } +} diff --git a/src/features/creators/newPackageProject.ts b/src/features/creators/newPackageProject.ts new file mode 100644 index 0000000..54da143 --- /dev/null +++ b/src/features/creators/newPackageProject.ts @@ -0,0 +1,197 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { commands, l10n, MarkdownString, QuickInputButtons, Uri, window, workspace } from 'vscode'; +import { PythonEnvironment, PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api'; +import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants'; +import { traceError } from '../../common/logging'; +import { showInputBoxWithButtons } from '../../common/window.apis'; +import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; +import { + isCopilotInstalled, + manageCopilotInstructionsFile, + manageLaunchJsonFile, + promptForVenv, + replaceInFilesAndNames, +} from './creationHelpers'; + +export class NewPackageProject implements PythonProjectCreator { + public readonly name = l10n.t('newPackage'); + public readonly displayName = l10n.t('Package'); + public readonly description = l10n.t('Creates a package folder in your current workspace'); + public readonly tooltip = new MarkdownString(l10n.t('Create a new Python package')); + + constructor( + private readonly envManagers: EnvironmentManagers, + private readonly projectManager: PythonProjectManager, + ) {} + + async create(options?: PythonProjectCreatorOptions): Promise { + let packageName = options?.name; + let createVenv: boolean | undefined; + let createCopilotInstructions: boolean | undefined; + if (options?.quickCreate === true) { + // If quickCreate is true, we should not prompt for any input + if (!packageName) { + throw new Error('Package name is required in quickCreate mode.'); + } + createVenv = true; + createCopilotInstructions = true; + } else { + //Prompt as quickCreate is false + if (!packageName) { + try { + packageName = await showInputBoxWithButtons({ + prompt: l10n.t('What is the name of the package? (e.g. my_package)'), + ignoreFocusOut: true, + showBackButton: true, + validateInput: (value) => { + // following PyPI (PEP 508) rules for package names + if (!/^([a-z_]|[a-z0-9_][a-z0-9._-]*[a-z0-9_])$/i.test(value)) { + return l10n.t( + 'Invalid package name. Use only letters, numbers, underscores, hyphens, or periods. Must start and end with a letter or number.', + ); + } + if (/^[-._0-9]$/i.test(value)) { + return l10n.t('Single-character package names cannot be a number, hyphen, or period.'); + } + return null; + }, + }); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + await commands.executeCommand('python-envs.createNewProjectFromTemplate'); + } + } + if (!packageName) { + return undefined; + } + // Use helper to prompt for virtual environment creation + const callback = () => { + return this.create(options); + }; + createVenv = await promptForVenv(callback); + if (createVenv === undefined) { + return undefined; + } + if (isCopilotInstalled()) { + createCopilotInstructions = true; + } + } + + // 1. Copy template folder + const newPackageTemplateFolder = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newPackageTemplate'); + if (!(await fs.pathExists(newPackageTemplateFolder))) { + window.showErrorMessage(l10n.t('Template folder does not exist, aborting creation.')); + return undefined; + } + + // Check if the destination folder is provided, otherwise use the first workspace folder + let destRoot = options?.rootUri.fsPath; + if (!destRoot) { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + window.showErrorMessage(l10n.t('No workspace folder is open or provided, aborting creation.')); + traceError(`Template file not found at: ${newPackageTemplateFolder}`); + return undefined; + } + destRoot = workspaceFolders[0].uri.fsPath; + } + + // Check if the destination folder already exists + const projectDestinationFolder = path.join(destRoot, `${packageName}_project`); + if (await fs.pathExists(projectDestinationFolder)) { + window.showErrorMessage( + l10n.t( + 'A project folder by that name already exists, aborting creation. Please retry with a unique package name given your workspace.', + ), + ); + return undefined; + } + await fs.copy(newPackageTemplateFolder, projectDestinationFolder); + + // 2. Replace 'package_name' in all files and file/folder names using a helper + await replaceInFilesAndNames(projectDestinationFolder, 'package_name', packageName); + + const createdPackage: PythonProject | undefined = { + name: packageName, + uri: Uri.file(projectDestinationFolder), + }; + // add package to list of packages + await this.projectManager.add(createdPackage); + + // 4. Create virtual environment if requested + let createdEnv: PythonEnvironment | undefined; + if (createVenv) { + // gets default environment manager + const en = this.envManagers.getEnvironmentManager(undefined); + if (en?.supportsQuickCreate) { + // opt to use quickCreate if available + createdEnv = await en.create(Uri.file(projectDestinationFolder), { quickCreate: true }); + } else if (!options?.quickCreate && en?.supportsCreate) { + // if quickCreate unavailable, use create method only if project is not quickCreate + createdEnv = await en.create(Uri.file(projectDestinationFolder), {}); + } else { + // get venv manager or any manager that supports quick creating environments + const venvManager = this.envManagers.managers.find( + (m) => m.id === 'ms-python.python:venv' || m.supportsQuickCreate, + ); + if (venvManager) { + createdEnv = await venvManager.create(Uri.file(projectDestinationFolder), { + quickCreate: true, + }); + } else { + const action = await window.showErrorMessage( + l10n.t( + 'A virtual environment could not be created for the new package "{0}" because your default environment manager does not support this operation and no alternative was available.', + packageName, + ), + l10n.t('Create custom environment'), + ); + if (action === l10n.t('Create custom environment')) { + await commands.executeCommand('python-envs.createAny', { + uri: createdPackage.uri, + selectEnvironment: true, + }); + createdEnv = await this.envManagers.getEnvironment(createdPackage.uri); + } + } + } + } + // 5. Get the Python environment for the destination folder if not already created + createdEnv = createdEnv || (await this.envManagers.getEnvironment(Uri.parse(projectDestinationFolder))); + if (!createdEnv) { + window.showErrorMessage( + l10n.t('Project created but unable to be correlated to correct Python environment.'), + ); + return undefined; + } + + // 6. Set the Python environment for the package + this.envManagers.setEnvironment(createdPackage?.uri, createdEnv); + + // 7. add custom github copilot instructions + if (createCopilotInstructions) { + const packageInstructionsPath = path.join( + NEW_PROJECT_TEMPLATES_FOLDER, + 'copilot-instructions-text', + 'package-copilot-instructions.md', + ); + await manageCopilotInstructionsFile(destRoot, packageInstructionsPath, [ + { searchValue: '', replaceValue: packageName }, + ]); + } + + // update launch.json file with config for the package + const launchJsonConfig = { + name: `Python Package: ${packageName}`, + type: 'debugpy', + request: 'launch', + module: packageName, + }; + await manageLaunchJsonFile(destRoot, JSON.stringify(launchJsonConfig)); + + return createdPackage; + } + return undefined; + } +} diff --git a/src/features/creators/newScriptProject.ts b/src/features/creators/newScriptProject.ts new file mode 100644 index 0000000..5a62b6c --- /dev/null +++ b/src/features/creators/newScriptProject.ts @@ -0,0 +1,130 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { commands, l10n, MarkdownString, QuickInputButtons, Uri, window, workspace } from 'vscode'; +import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api'; +import { NEW_PROJECT_TEMPLATES_FOLDER } from '../../common/constants'; +import { traceError } from '../../common/logging'; +import { showInputBoxWithButtons, showTextDocument } from '../../common/window.apis'; +import { PythonProjectManager } from '../../internal.api'; +import { isCopilotInstalled, manageCopilotInstructionsFile, replaceInFilesAndNames } from './creationHelpers'; + +export class NewScriptProject implements PythonProjectCreator { + public readonly name = l10n.t('newScript'); + public readonly displayName = l10n.t('Script'); + public readonly description = l10n.t('Creates a new script folder in your current workspace'); + public readonly tooltip = new MarkdownString(l10n.t('Create a new Python script')); + + constructor(private readonly projectManager: PythonProjectManager) {} + + async create(options?: PythonProjectCreatorOptions): Promise { + // quick create (needs name, will always create venv and copilot instructions) + // not quick create + // ask for script file name + // ask if they want venv + let scriptFileName = options?.name; + let createCopilotInstructions: boolean | undefined; + if (options?.quickCreate === true) { + // If quickCreate is true, we should not prompt for any input + if (!scriptFileName) { + throw new Error('Script file name is required in quickCreate mode.'); + } + createCopilotInstructions = true; + } else { + //Prompt as quickCreate is false + if (!scriptFileName) { + try { + scriptFileName = await showInputBoxWithButtons({ + prompt: l10n.t('What is the name of the script? (e.g. my_script.py)'), + ignoreFocusOut: true, + showBackButton: true, + validateInput: (value) => { + // Ensure the filename ends with .py and follows valid naming conventions + if (!value.endsWith('.py')) { + return l10n.t('Script name must end with ".py".'); + } + const baseName = value.replace(/\.py$/, ''); + // following PyPI (PEP 508) rules for package names + if (!/^([a-z_]|[a-z0-9_][a-z0-9._-]*[a-z0-9_])$/i.test(baseName)) { + return l10n.t( + 'Invalid script name. Use only letters, numbers, underscores, hyphens, or periods. Must start and end with a letter or number.', + ); + } + if (/^[-._0-9]$/i.test(baseName)) { + return l10n.t('Single-character script names cannot be a number, hyphen, or period.'); + } + return null; + }, + }); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + await commands.executeCommand('python-envs.createNewProjectFromTemplate'); + } + } + if (!scriptFileName) { + return undefined; + } + if (isCopilotInstalled()) { + createCopilotInstructions = true; + } + } + + // 1. Copy template folder + const newScriptTemplateFile = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'new723ScriptTemplate', 'script.py'); + if (!(await fs.pathExists(newScriptTemplateFile))) { + window.showErrorMessage(l10n.t('Template file does not exist, aborting creation.')); + traceError(`Template file not found at: ${newScriptTemplateFile}`); + return undefined; + } + + // Check if the destination folder is provided, otherwise use the first workspace folder + let destRoot = options?.rootUri.fsPath; + if (!destRoot) { + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + window.showErrorMessage(l10n.t('No workspace folder is open or provided, aborting creation.')); + return undefined; + } + destRoot = workspaceFolders[0].uri.fsPath; + } + + // Check if the destination folder already exists + const scriptDestination = path.join(destRoot, scriptFileName); + if (await fs.pathExists(scriptDestination)) { + window.showErrorMessage( + l10n.t( + 'A script file by that name already exists, aborting creation. Please retry with a unique script name given your workspace.', + ), + ); + return undefined; + } + await fs.copy(newScriptTemplateFile, scriptDestination); + + // 2. Replace 'script_name' in the file using a helper (just script name remove .py) + await replaceInFilesAndNames(scriptDestination, 'script_name', scriptFileName.replace(/\.py$/, '')); + + // 3. add custom github copilot instructions + if (createCopilotInstructions) { + const packageInstructionsPath = path.join( + NEW_PROJECT_TEMPLATES_FOLDER, + 'copilot-instructions-text', + 'script-copilot-instructions.md', + ); + await manageCopilotInstructionsFile(destRoot, packageInstructionsPath, [ + { searchValue: '', replaceValue: scriptFileName }, + ]); + } + + // Add the created script to the project manager + const createdScript: PythonProject | undefined = { + name: scriptFileName, + uri: Uri.file(scriptDestination), + }; + this.projectManager.add(createdScript); + + await showTextDocument(createdScript.uri); + + return createdScript; + } + return undefined; + } +} diff --git a/src/features/creators/projectCreators.ts b/src/features/creators/projectCreators.ts new file mode 100644 index 0000000..0c52d43 --- /dev/null +++ b/src/features/creators/projectCreators.ts @@ -0,0 +1,21 @@ +import { Disposable } from 'vscode'; +import { PythonProjectCreator } from '../../api'; +import { ProjectCreators } from '../../internal.api'; + +export class ProjectCreatorsImpl implements ProjectCreators { + private _creators: PythonProjectCreator[] = []; + + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { + this._creators.push(creator); + return new Disposable(() => { + this._creators = this._creators.filter((item) => item !== creator); + }); + } + getProjectCreators(): PythonProjectCreator[] { + return this._creators; + } + + dispose() { + this._creators = []; + } +} diff --git a/src/features/envCommands.ts b/src/features/envCommands.ts index 927109c..fe02e02 100644 --- a/src/features/envCommands.ts +++ b/src/features/envCommands.ts @@ -1,47 +1,48 @@ -import { TaskExecution, TaskRevealKind, Terminal, Uri, window } from 'vscode'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { commands, QuickInputButtons, TaskExecution, TaskRevealKind, Terminal, Uri, workspace } from 'vscode'; +import { + CreateEnvironmentOptions, + PythonEnvironment, + PythonEnvironmentApi, + PythonProject, + PythonProjectCreator, + PythonProjectCreatorOptions, +} from '../api'; +import { traceError, traceInfo, traceVerbose } from '../common/logging'; import { EnvironmentManagers, + InternalEnvironmentManager, InternalPackageManager, ProjectCreators, PythonProjectManager, - PythonTaskExecutionOptions, - PythonTerminalExecutionOptions, } from '../internal.api'; -import { traceError, traceVerbose } from '../common/logging'; +import { removePythonProjectSetting, setEnvironmentManager, setPackageManager } from './settings/settingHelpers'; + +import { clipboardWriteText } from '../common/env.apis'; +import {} from '../common/errors/utils'; +import { pickEnvironment } from '../common/pickers/environments'; import { - getPackagesToInstall, - getPackagesToUninstall, pickCreator, - pickEnvironment, pickEnvironmentManager, pickPackageManager, - pickPackageOptions, - pickProject, -} from '../common/pickers'; -import { PythonEnvironment, PythonEnvironmentApi, PythonProject, PythonProjectCreator } from '../api'; -import * as path from 'path'; -import { - setEnvironmentManager, - setPackageManager, - addPythonProjectSetting, - removePythonProjectSetting, - getDefaultEnvManagerSetting, - getDefaultPkgManagerSetting, -} from './settings/settingHelpers'; - -import { getAbsolutePath } from '../common/utils/fileNameUtils'; -import { createPythonTerminal } from './execution/terminal'; -import { runInTerminal } from './execution/runInTerminal'; + pickWorkspaceFolder, +} from '../common/pickers/managers'; +import { pickProject, pickProjectMany } from '../common/pickers/projects'; +import { activeTextEditor, showErrorMessage, showInformationMessage } from '../common/window.apis'; import { runAsTask } from './execution/runAsTask'; +import { runInTerminal } from './terminal/runInTerminal'; +import { TerminalManager } from './terminal/terminalManager'; import { EnvManagerTreeItem, - PackageRootTreeItem, - PythonEnvTreeItem, - ProjectItem, + EnvTreeItemKind, + GlobalProjectItem, + PackageTreeItem, ProjectEnvironment, - ProjectPackageRootTreeItem, + ProjectItem, + ProjectPackage, + PythonEnvTreeItem, } from './views/treeViewItems'; -import { Common } from '../common/localize'; export async function refreshManagerCommand(context: unknown): Promise { if (context instanceof EnvManagerTreeItem) { @@ -52,54 +53,140 @@ export async function refreshManagerCommand(context: unknown): Promise { } } -export async function refreshPackagesCommand(context: unknown) { - if (context instanceof ProjectPackageRootTreeItem) { - const view = context as ProjectPackageRootTreeItem; - const manager = view.manager; - await manager.refresh(view.environment); - } else if (context instanceof PackageRootTreeItem) { - const view = context as PackageRootTreeItem; - const manager = view.manager; - await manager.refresh(view.environment); +export async function refreshPackagesCommand(context: unknown, managers?: EnvironmentManagers): Promise { + if (context instanceof ProjectEnvironment) { + const view = context as ProjectEnvironment; + if (managers) { + const pkgManager = managers.getPackageManager(view.parent.project.uri); + if (pkgManager) { + await pkgManager.refresh(view.environment); + } + } + } else if (context instanceof PythonEnvTreeItem) { + const view = context as PythonEnvTreeItem; + const envManager = + view.parent.kind === EnvTreeItemKind.environmentGroup ? view.parent.parent.manager : view.parent.manager; + const pkgManager = managers?.getPackageManager(envManager.preferredPackageManagerId); + if (pkgManager) { + await pkgManager.refresh(view.environment); + } } else { traceVerbose(`Invalid context for refresh command: ${context}`); } } +/** + * Creates a Python environment using the manager implied by the context (no user prompt). + */ export async function createEnvironmentCommand( context: unknown, - managers: EnvironmentManagers, - projects: PythonProjectManager, -): Promise { + em: EnvironmentManagers, + pm: PythonProjectManager, +): Promise { if (context instanceof EnvManagerTreeItem) { const manager = (context as EnvManagerTreeItem).manager; - await manager.create('global'); + const projects = pm.getProjects(); + if (projects.length === 0) { + const env = await manager.create('global', undefined); + if (env) { + await em.setEnvironments('global', env); + } + return env; + } else if (projects.length > 0) { + const selected = await pickProjectMany(projects); + if (selected) { + const scope = selected.length === 0 ? 'global' : selected.map((p) => p.uri); + const env = await manager.create(scope, undefined); + if (env) { + await em.setEnvironmentsIfUnset(scope, env); + } + return env; + } else { + traceInfo('No project selected or global condition met for environment creation'); + } + } } else if (context instanceof Uri) { - const manager = managers.getEnvironmentManager(context as Uri); - const project = projects.get(context as Uri); + const manager = em.getEnvironmentManager(context as Uri); + const project = pm.get(context as Uri); if (project) { - await manager?.create(project); + return await manager?.create(project.uri, undefined); + } else { + traceError(`No project found for ${context}`); } } else { traceError(`Invalid context for create command: ${context}`); } } +/** + * Prompts the user to pick the environment manager and project(s) for environment creation. + */ export async function createAnyEnvironmentCommand( - managers: EnvironmentManagers, - projects: PythonProjectManager, -): Promise { - const pw = await pickProject(projects.getProjects()); - if (pw) { - const defaultManager = managers.getEnvironmentManager(pw.uri); - const managerId = await pickEnvironmentManager( - managers.managers.filter((m) => m.supportsCreate), - defaultManager?.supportsCreate ? defaultManager : undefined, - ); - - const manager = managers.managers.find((m) => m.id === managerId); + em: EnvironmentManagers, + pm: PythonProjectManager, + options?: CreateEnvironmentOptions & { + selectEnvironment?: boolean; + showBackButton?: boolean; + uri?: Uri; + }, +): Promise { + const select = options?.selectEnvironment; + const projects = pm.getProjects(options?.uri ? [options?.uri] : undefined); + if (projects.length === 0) { + const managerId = await pickEnvironmentManager(em.managers.filter((m) => m.supportsCreate)); + const manager = em.managers.find((m) => m.id === managerId); if (manager) { - await manager.create(pw); + const env = await manager.create('global', { ...options }); + if (select && env) { + await manager.set(undefined, env); + } + return env; + } + } else if (projects.length > 0) { + const selected = await pickProjectMany(projects, options?.showBackButton); + + if (selected && selected.length > 0) { + const defaultManagers: InternalEnvironmentManager[] = []; + + selected.forEach((p) => { + const manager = em.getEnvironmentManager(p.uri); + if (manager && manager.supportsCreate && !defaultManagers.includes(manager)) { + defaultManagers.push(manager); + } + }); + + let quickCreate = options?.quickCreate ?? false; + let manager: InternalEnvironmentManager | undefined; + + if (quickCreate && defaultManagers.length === 1) { + manager = defaultManagers[0]; + } else { + let managerId = await pickEnvironmentManager( + em.managers.filter((m) => m.supportsCreate), + defaultManagers, + options?.showBackButton, + ); + if (managerId?.startsWith('QuickCreate#')) { + quickCreate = true; + managerId = managerId.replace('QuickCreate#', ''); + } + + manager = em.managers.find((m) => m.id === managerId); + } + + if (manager) { + const env = await manager.create( + selected.map((p) => p.uri), + { ...options, quickCreate }, + ); + if (select && env) { + await em.setEnvironments( + selected.map((p) => p.uri), + env, + ); + } + return env; + } } } } @@ -107,7 +194,8 @@ export async function createAnyEnvironmentCommand( export async function removeEnvironmentCommand(context: unknown, managers: EnvironmentManagers): Promise { if (context instanceof PythonEnvTreeItem) { const view = context as PythonEnvTreeItem; - const manager = view.parent.manager; + const manager = + view.parent.kind === EnvTreeItemKind.environmentGroup ? view.parent.parent.manager : view.parent.manager; await manager.remove(view.environment); } else if (context instanceof Uri) { const manager = managers.getEnvironmentManager(context as Uri); @@ -115,214 +203,267 @@ export async function removeEnvironmentCommand(context: unknown, managers: Envir if (environment) { await manager?.remove(environment); } + } else if (context instanceof ProjectEnvironment) { + const view = context as ProjectEnvironment; + const manager = managers.getEnvironmentManager(view.parent.project.uri); + await manager?.remove(view.environment); } else { traceError(`Invalid context for remove command: ${context}`); } } -export async function handlePackagesCommand( - packageManager: InternalPackageManager, - environment: PythonEnvironment, - packages?: string[], -): Promise { - const action = await pickPackageOptions(); - - if (action === Common.install) { - if (!packages || packages.length === 0) { - packages = await getPackagesToInstall(packageManager, environment); - } - if (packages && packages.length > 0) { - return packageManager.install(environment, packages, { upgrade: false }); - } - } - - if (action === Common.uninstall) { - if (!packages || packages.length === 0) { - const allPackages = await packageManager.getPackages(environment); - if (allPackages && allPackages.length > 0) { - packages = (await getPackagesToUninstall(allPackages))?.map((p) => p.name); - } - - if (packages && packages.length > 0) { - return packageManager.uninstall(environment, packages); - } - } +export async function handlePackageUninstall(context: unknown, em: EnvironmentManagers) { + if (context instanceof PackageTreeItem || context instanceof ProjectPackage) { + const moduleName = context.pkg.name; + const environment = context instanceof ProjectPackage ? context.parent.environment : context.parent.environment; + const packageManager = em.getPackageManager(environment); + await packageManager?.manage(environment, { uninstall: [moduleName], install: [] }); + return; } -} - -export interface EnvironmentSetResult { - workspace?: PythonProject; - environment: PythonEnvironment; + traceError(`Invalid context for uninstall command: ${typeof context}`); } export async function setEnvironmentCommand( context: unknown, em: EnvironmentManagers, wm: PythonProjectManager, -): Promise { +): Promise { if (context instanceof PythonEnvTreeItem) { - const view = context as PythonEnvTreeItem; - const manager = view.parent.manager; - const pw = await pickProject(wm.getProjects()); - if (pw) { - await setEnvironmentManager(pw.uri, manager.id, wm); - await setPackageManager(pw.uri, manager.preferredPackageManagerId, wm); - manager.set(pw.uri, view.environment); - return { workspace: pw, environment: view.environment }; + try { + const view = context as PythonEnvTreeItem; + const projects = wm.getProjects(); + if (projects.length > 0) { + const selected = await pickProjectMany(projects); + if (selected && selected.length > 0) { + // Check if the selected environment is already the current one for each project + await setEnvironmentForProjects(selected, context.environment, em); + } + } else { + await em.setEnvironments('global', view.environment); + } + } catch (ex) { + if (ex === QuickInputButtons.Back) { + await setEnvironmentCommand(context, em, wm); + } + throw ex; } - return; } else if (context instanceof ProjectItem) { const view = context as ProjectItem; - return setEnvironmentCommand(view.project.uri, em, wm); + await setEnvironmentCommand([view.project.uri], em, wm); + } else if (context instanceof GlobalProjectItem) { + await setEnvironmentCommand(undefined, em, wm); } else if (context instanceof Uri) { - const uri = context as Uri; - const manager = em.getEnvironmentManager(uri); - const recommended = await manager?.get(uri); - const result = await pickEnvironment( - em.managers, - 'all', - manager && recommended ? { selected: recommended, manager: manager } : undefined, - ); - if (result) { - result.manager.set(uri, result.selected); - if (result.manager.id !== manager?.id) { - await setEnvironmentManager(uri, result.manager.id, wm); - await setPackageManager(uri, result.manager.preferredPackageManagerId, wm); + await setEnvironmentCommand([context], em, wm); + } else if (context === undefined) { + try { + const projects = wm.getProjects(); + if (projects.length > 0) { + const selected = await pickProjectMany(projects); + if (selected && selected.length > 0) { + const uris = selected.map((p) => p.uri); + await setEnvironmentCommand(uris, em, wm); + } + } else { + const globalEnvManager = em.getEnvironmentManager(undefined); + const recommended = globalEnvManager ? await globalEnvManager.get(undefined) : undefined; + const selected = await pickEnvironment(em.managers, globalEnvManager ? [globalEnvManager] : [], { + projects: [], + recommended, + showBackButton: false, + }); + if (selected) { + await em.setEnvironments('global', selected); + } + } + } catch (ex) { + if (ex === QuickInputButtons.Back) { + await setEnvironmentCommand(context, em, wm); } - return { workspace: wm.get(uri), environment: result.selected }; + throw ex; } - return; - } else if (context === undefined) { - const project = await pickProject(wm.getProjects()); - if (project) { - return setEnvironmentCommand(project.uri, em, wm); + } else if (Array.isArray(context) && context.length > 0 && context.every((c) => c instanceof Uri)) { + const uris = context as Uri[]; + const projects = wm.getProjects(uris).map((p) => p); + const projectEnvManagers = em.getProjectEnvManagers(uris); + const recommended = + projectEnvManagers.length === 1 && uris.length === 1 ? await projectEnvManagers[0].get(uris[0]) : undefined; + const selected = await pickEnvironment(em.managers, projectEnvManagers, { + projects, + recommended, + showBackButton: uris.length > 1, + }); + + if (selected) { + // Use the same logic for checking already set environments + await setEnvironmentForProjects(projects, selected, em); } - return; + } else { + traceError(`Invalid context for setting environment command: ${context}`); + showErrorMessage('Invalid context for setting environment'); } - traceError(`Invalid context for setting environment command: ${context}`); - window.showErrorMessage('Invalid context for setting environment'); } - -export async function resetEnvironmentCommand( - context: unknown, +/** + * Sets the environment for the given projects, showing a warning for those already set. + * @param selectedProjects Array of PythonProject selected by user + * @param environment The environment to set for the projects + * @param em The EnvironmentManagers instance + */ +async function setEnvironmentForProjects( + selectedProjects: PythonProject[], + environment: PythonEnvironment, em: EnvironmentManagers, - wm: PythonProjectManager, -): Promise { - if (context instanceof ProjectItem) { - const view = context as ProjectItem; - return resetEnvironmentCommand(view.project.uri, em, wm); - } else if (context instanceof Uri) { - const uri = context as Uri; - const manager = em.getEnvironmentManager(uri); - if (manager) { - manager.set(uri, undefined); - } else { - window.showErrorMessage(`No environment manager found for: ${uri.fsPath}`); - traceError(`No environment manager found for ${uri.fsPath}`); - } - return; - } else if (context === undefined) { - const pw = await pickProject(wm.getProjects()); - if (pw) { - return resetEnvironmentCommand(pw.uri, em, wm); +) { + let alreadySet: PythonProject[] = []; + for (const p of selectedProjects) { + const currentEnv = await em.getEnvironment(p.uri); + if (currentEnv?.envId.id === environment.envId.id) { + alreadySet.push(p); } + } + if (alreadySet.length > 0) { + const env = alreadySet.length > 1 ? 'environments' : 'environment'; + showInformationMessage( + `"${environment.name}" is already selected as the ${env} for: ${alreadySet + .map((p) => `"${p.name}"`) + .join(', ')}`, + ); + } + const toSet: PythonProject[] = selectedProjects.filter((p) => !alreadySet.includes(p)); + const uris = toSet.map((p) => p.uri); + if (uris.length === 0) { return; } - traceError(`Invalid context for unset environment command: ${context}`); - window.showErrorMessage('Invalid context for unset environment'); + await em.setEnvironments(uris, environment); } export async function setEnvManagerCommand(em: EnvironmentManagers, wm: PythonProjectManager): Promise { - const pw = await pickProject(wm.getProjects()); - if (pw) { + const projects = await pickProjectMany(wm.getProjects()); + if (projects && projects.length > 0) { const manager = await pickEnvironmentManager(em.managers); if (manager) { - await setEnvironmentManager(pw.uri, manager, wm); + await setEnvironmentManager(projects.map((p) => ({ project: p, envManager: manager }))); } } } -export async function setPkgManagerCommand(em: EnvironmentManagers, wm: PythonProjectManager): Promise { - const pw = await pickProject(wm.getProjects()); - if (pw) { +export async function setPackageManagerCommand(em: EnvironmentManagers, wm: PythonProjectManager): Promise { + const projects = await pickProjectMany(wm.getProjects()); + if (projects && projects.length > 0) { const manager = await pickPackageManager(em.packageManagers); if (manager) { - await setPackageManager(pw.uri, manager, wm); + await setPackageManager(projects.map((p) => ({ project: p, packageManager: manager }))); } } } -export async function addPythonProject( +/** + * Creates a new Python project using a selected PythonProjectCreator. + * + * This function calls create on the selected creator and handles the creation process. Will return + * without doing anything if the resource is a ProjectItem, as the project is already created. + * + * @param resource - The resource to use for project creation (can be a Uri(s), ProjectItem(s), or undefined). + * @param wm - The PythonProjectManager instance for managing projects. + * @param em - The EnvironmentManagers instance for managing environments. + * @param pc - The ProjectCreators instance for accessing available project creators. + * @returns A promise that resolves when the project has been created, or void if cancelled or invalid. + */ +export async function addPythonProjectCommand( resource: unknown, wm: PythonProjectManager, em: EnvironmentManagers, pc: ProjectCreators, -): Promise { +): Promise { if (wm.getProjects().length === 0) { - window.showErrorMessage('Please open a folder/project before adding a workspace'); - return; - } - - if (resource instanceof Uri) { - const uri = resource as Uri; - const envManagerId = getDefaultEnvManagerSetting(wm, uri); - const pkgManagerId = getDefaultPkgManagerSetting( - wm, - uri, - em.getEnvironmentManager(envManagerId)?.preferredPackageManagerId, + const r = await showErrorMessage( + 'Please open a folder/project to create a Python project.', + { + modal: true, + }, + 'Open Folder', ); - const pw = wm.create(path.basename(uri.fsPath), uri); - await addPythonProjectSetting(pw, envManagerId, pkgManagerId); - wm.add(pw); - return pw; - } - - if (resource === undefined) { - const creator: PythonProjectCreator | undefined = await pickCreator(pc.getProjectCreators()); - if (!creator) { + if (r === 'Open Folder') { + await commands.executeCommand('vscode.openFolder'); return; } - - let results = await creator.create(); - if (results === undefined) { + } + if (resource instanceof Array) { + for (const r of resource) { + await addPythonProjectCommand(r, wm, em, pc); return; } + } + if (resource instanceof ProjectItem) { + // If the context is a ProjectItem, project is already created. Just add it to the package manager project list. + wm.add(resource.project); + return; + } + let options: PythonProjectCreatorOptions | undefined; - if (!Array.isArray(results)) { - results = [results]; - } - - if (Array.isArray(results)) { - if (results.length === 0) { + if (resource instanceof Uri) { + // Use resource as the URI for the project if it is a URI. + options = { + name: resource.fsPath, + rootUri: resource, + }; + + // When a URI is provided (right-click in explorer), directly use the existingProjects creator + const existingProjectsCreator = pc.getProjectCreators().find((c) => c.name === 'existingProjects'); + if (existingProjectsCreator) { + try { + if (existingProjectsCreator.supportsQuickCreate) { + options = { + ...options, + quickCreate: true, + }; + } + await existingProjectsCreator.create(options); + // trigger refresh to populate environments within the new project + await Promise.all(em.managers.map((m) => m.refresh(options?.rootUri))); return; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + return addPythonProjectCommand(resource, wm, em, pc); + } + throw ex; } } + } - const projects: PythonProject[] = []; + // If not a URI or existingProjectsCreator not found, fall back to picker + const creator: PythonProjectCreator | undefined = await pickCreator(pc.getProjectCreators()); + if (!creator) { + return; + } - for (const result of results) { - const uri = await getAbsolutePath(result.uri.fsPath); - if (!uri) { - traceError(`Path does not belong to any opened workspace: ${result.uri.fsPath}`); - continue; + // if multiroot, prompt the user to select which workspace to create the project in + const workspaceFolders = workspace.workspaceFolders; + if (!resource && workspaceFolders && workspaceFolders.length > 1) { + try { + const workspace = await pickWorkspaceFolder(true); + resource = workspace?.uri; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + return addPythonProjectCommand(resource, wm, em, pc); } + throw ex; + } + } - const envManagerId = getDefaultEnvManagerSetting(wm, uri); - const pkgManagerId = getDefaultPkgManagerSetting( - wm, - uri, - em.getEnvironmentManager(envManagerId)?.preferredPackageManagerId, - ); - const pw = wm.create(path.basename(uri.fsPath), uri); - await addPythonProjectSetting(pw, envManagerId, pkgManagerId); - projects.push(pw); + try { + await creator.create(options); + // trigger refresh to populate environments within the new project + await Promise.all(em.managers.map((m) => m.refresh(options?.rootUri))); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + return addPythonProjectCommand(resource, wm, em, pc); } - return projects; + throw ex; } } export async function removePythonProject(item: ProjectItem, wm: PythonProjectManager): Promise { - await removePythonProjectSetting(item.project); + await removePythonProjectSetting([{ project: item.project }]); wm.remove(item.project); } @@ -371,19 +512,38 @@ export async function getPackageCommandOptions( export async function createTerminalCommand( context: unknown, api: PythonEnvironmentApi, + tm: TerminalManager, ): Promise { - if (context instanceof Uri) { + if (context === undefined) { + const pw = await pickProject(api.getPythonProjects()); + if (pw) { + const env = await api.getEnvironment(pw.uri); + const cwd = await findParentIfFile(pw.uri.fsPath); + if (env) { + return await tm.create(env, { cwd }); + } + } + } else if (context instanceof Uri) { const uri = context as Uri; const env = await api.getEnvironment(uri); const pw = api.getPythonProject(uri); if (env && pw) { - return await createPythonTerminal(env, pw.uri); + const cwd = await findParentIfFile(pw.uri.fsPath); + return await tm.create(env, { cwd }); } } else if (context instanceof ProjectItem) { const view = context as ProjectItem; const env = await api.getEnvironment(view.project.uri); + const cwd = await findParentIfFile(view.project.uri.fsPath); + if (env) { + const terminal = await tm.create(env, { cwd }); + terminal.show(); + return terminal; + } + } else if (context instanceof GlobalProjectItem) { + const env = await api.getEnvironment(undefined); if (env) { - const terminal = await createPythonTerminal(env, view.project.uri); + const terminal = await tm.create(env, { cwd: undefined }); terminal.show(); return terminal; } @@ -391,65 +551,117 @@ export async function createTerminalCommand( const view = context as PythonEnvTreeItem; const pw = await pickProject(api.getPythonProjects()); if (pw) { - const terminal = await createPythonTerminal(view.environment, pw.uri); + const cwd = await findParentIfFile(pw.uri.fsPath); + const terminal = await tm.create(view.environment, { cwd }); terminal.show(); return terminal; } } } -export async function runInTerminalCommand(item: unknown, api: PythonEnvironmentApi): Promise { - const keys = Object.keys(item ?? {}); +export async function findParentIfFile(cwd: string): Promise { + const stat = await fs.stat(cwd); + if (stat.isFile()) { + // If the project is a file, use the directory of the file as the cwd + return path.dirname(cwd); + } + return cwd; +} + +export async function runInTerminalCommand( + item: unknown, + api: PythonEnvironmentApi, + tm: TerminalManager, +): Promise { if (item instanceof Uri) { const uri = item as Uri; const project = api.getPythonProject(uri); const environment = await api.getEnvironment(uri); if (environment && project) { - await runInTerminal( - { - project, - args: [item.fsPath], - }, - environment, - { show: true }, - ); + const resolvedEnv = await api.resolveEnvironment(environment.environmentPath); + const envFinal = resolvedEnv ?? environment; + const terminal = await tm.getProjectTerminal(project, envFinal); + await runInTerminal(envFinal, terminal, { + cwd: project.uri, + args: [item.fsPath], + show: true, + }); } - } else if (keys.includes('project') && keys.includes('args')) { - const options = item as PythonTerminalExecutionOptions; - const environment = await api.getEnvironment(options.project.uri); - if (environment) { - await runInTerminal(options, environment, { show: true }); + } + throw new Error(`Invalid context for run-in-terminal: ${item}`); +} + +export async function runInDedicatedTerminalCommand( + item: unknown, + api: PythonEnvironmentApi, + tm: TerminalManager, +): Promise { + if (item instanceof Uri) { + const uri = item as Uri; + const project = api.getPythonProject(uri); + const environment = await api.getEnvironment(uri); + + if (environment && project) { + const resolvedEnv = await api.resolveEnvironment(environment.environmentPath); + const envFinal = resolvedEnv ?? environment; + const terminal = await tm.getDedicatedTerminal(item, project, envFinal); + await runInTerminal(envFinal, terminal, { + cwd: project.uri, + args: [item.fsPath], + show: true, + }); } } + throw new Error(`Invalid context for run-in-terminal: ${item}`); } export async function runAsTaskCommand(item: unknown, api: PythonEnvironmentApi): Promise { - const keys = Object.keys(item ?? {}); if (item instanceof Uri) { const uri = item as Uri; const project = api.getPythonProject(uri); const environment = await api.getEnvironment(uri); - if (environment && project) { + if (environment) { + const resolvedEnv = await api.resolveEnvironment(environment.environmentPath); + const envFinal = resolvedEnv ?? environment; return await runAsTask( + envFinal, { project, args: [item.fsPath], name: 'Python Run', }, - environment, + { reveal: TaskRevealKind.Always }, ); } - } else if (keys.includes('project') && keys.includes('args') && keys.includes('name')) { - const options = item as PythonTaskExecutionOptions; - const environment = await api.getEnvironment(options.project.uri); - if (environment) { - return await runAsTask(options, environment); - } } else if (item === undefined) { - const uri = window.activeTextEditor?.document.uri; + const uri = activeTextEditor()?.document.uri; if (uri) { return runAsTaskCommand(uri, api); } } } + +export async function copyPathToClipboard(item: unknown): Promise { + if (item instanceof ProjectItem) { + const projectPath = item.project.uri.fsPath; + await clipboardWriteText(projectPath); + traceInfo(`Copied project path to clipboard: ${projectPath}`); + } else if (item instanceof ProjectEnvironment || item instanceof PythonEnvTreeItem) { + const run = item.environment.execInfo.run; + const envPath = run.executable; + await clipboardWriteText(envPath); + traceInfo(`Copied environment path to clipboard: ${envPath}`); + } else { + traceVerbose(`Invalid context for copy path to clipboard: ${item}`); + } +} + +export async function revealProjectInExplorer(item: unknown): Promise { + if (item instanceof ProjectItem) { + const projectUri = item.project.uri; + await commands.executeCommand('revealInExplorer', projectUri); + } else { + traceVerbose(`Invalid context for reveal project in explorer: ${item}`); + } +} diff --git a/src/features/envManagers.ts b/src/features/envManagers.ts index 45ef040..b78e8bc 100644 --- a/src/features/envManagers.ts +++ b/src/features/envManagers.ts @@ -1,14 +1,23 @@ -import { Disposable, EventEmitter, Uri, workspace, ConfigurationTarget, Event } from 'vscode'; +import { ConfigurationTarget, Disposable, Event, EventEmitter, Uri, workspace } from 'vscode'; import { DidChangeEnvironmentEventArgs, DidChangeEnvironmentsEventArgs, DidChangePackagesEventArgs, EnvironmentManager, + GetEnvironmentScope, PackageManager, + PythonEnvironment, PythonProject, + SetEnvironmentScope, } from '../api'; -import { traceError } from '../common/logging'; -import { getDefaultEnvManagerSetting, getDefaultPkgManagerSetting } from './settings/settingHelpers'; +import { + EnvironmentManagerAlreadyRegisteredError, + PackageManagerAlreadyRegisteredError, +} from '../common/errors/AlreadyRegisteredError'; +import { traceError, traceVerbose } from '../common/logging'; +import { EventNames } from '../common/telemetry/constants'; +import { sendTelemetryEvent } from '../common/telemetry/sender'; +import { getCallingExtension } from '../common/utils/frameUtils'; import { DidChangeEnvironmentManagerEventArgs, DidChangePackageManagerEventArgs, @@ -22,24 +31,31 @@ import { PythonProjectManager, PythonProjectSettings, } from '../internal.api'; -import { getCallingExtension } from '../common/extensions.apis'; import { - EnvironmentManagerAlreadyRegisteredError, - PackageManagerAlreadyRegisteredError, -} from '../common/errors/AlreadyRegisteredError'; + EditAllManagerSettings, + getDefaultEnvManagerSetting, + getDefaultPkgManagerSetting, + setAllManagerSettings, +} from './settings/settingHelpers'; function generateId(name: string): string { - return `${getCallingExtension()}:${name}`; + const newName = name.toLowerCase().replace(/[^a-zA-Z0-9-_]/g, '_'); + if (name !== newName) { + traceVerbose(`Environment manager name "${name}" was normalized to "${newName}"`); + } + return `${getCallingExtension()}:${newName}`; } export class PythonEnvironmentManagers implements EnvironmentManagers { private _environmentManagers: Map = new Map(); private _packageManagers: Map = new Map(); + private readonly _previousEnvironments = new Map(); private _onDidChangeEnvironmentManager = new EventEmitter(); private _onDidChangePackageManager = new EventEmitter(); private _onDidChangeEnvironments = new EventEmitter(); private _onDidChangeEnvironment = new EventEmitter(); + private _onDidChangeEnvironmentFiltered = new EventEmitter(); private _onDidChangePackages = new EventEmitter(); public onDidChangeEnvironmentManager: Event = @@ -48,8 +64,10 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { public onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; public onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; public onDidChangePackages: Event = this._onDidChangePackages.event; + public onDidChangeEnvironmentFiltered: Event = + this._onDidChangeEnvironmentFiltered.event; - constructor(private readonly workspaceManager: PythonProjectManager) {} + constructor(private readonly pm: PythonProjectManager) {} public registerEnvironmentManager(manager: EnvironmentManager): Disposable { const managerId = generateId(manager.name); @@ -74,7 +92,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { ); }), mgr.onDidChangeEnvironment((e: DidChangeEnvironmentEventArgs) => { - if (e.old === undefined && e.new === undefined) { + if (e.old?.envId.id === e.new?.envId.id) { return; } @@ -84,6 +102,13 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { this._environmentManagers.set(managerId, mgr); this._onDidChangeEnvironmentManager.fire({ kind: 'registered', manager: mgr }); + + if (!managerId.toLowerCase().startsWith('undefined_publisher.')) { + sendTelemetryEvent(EventNames.ENVIRONMENT_MANAGER_REGISTERED, undefined, { + managerId, + }); + } + return new Disposable(() => { this._environmentManagers.delete(managerId); disposables.forEach((d) => d.dispose()); @@ -117,6 +142,13 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { this._packageManagers.set(managerId, mgr); this._onDidChangePackageManager.fire({ kind: 'registered', manager: mgr }); + + if (!managerId.toLowerCase().startsWith('undefined_publisher.')) { + sendTelemetryEvent(EventNames.PACKAGE_MANAGER_REGISTERED, undefined, { + managerId, + }); + } + return new Disposable(() => { this._packageManagers.delete(managerId); disposables.forEach((d) => d.dispose()); @@ -133,6 +165,10 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { this._onDidChangePackages.dispose(); } + /** + * Returns the environment manager for the given context. + * Uses the default from settings if context is undefined or a Uri; otherwise uses the id or environment's managerId passed in via context. + */ public getEnvironmentManager(context: EnvironmentManagerScope): InternalEnvironmentManager | undefined { if (this._environmentManagers.size === 0) { traceError('No environment managers registered'); @@ -141,7 +177,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { if (context === undefined || context instanceof Uri) { // get default environment manager from setting - const defaultEnvManagerId = getDefaultEnvManagerSetting(this.workspaceManager, context); + const defaultEnvManagerId = getDefaultEnvManagerSetting(this.pm, context); if (defaultEnvManagerId === undefined) { return undefined; } @@ -162,8 +198,8 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { } if (context === undefined || context instanceof Uri) { - const defaultPkgManagerId = getDefaultPkgManagerSetting(this.workspaceManager, context); - const defaultEnvManagerId = getDefaultEnvManagerSetting(this.workspaceManager, context); + const defaultPkgManagerId = getDefaultPkgManagerSetting(this.pm, context); + const defaultEnvManagerId = getDefaultEnvManagerSetting(this.pm, context); if (defaultPkgManagerId) { return this._packageManagers.get(defaultPkgManagerId); } @@ -223,4 +259,230 @@ export class PythonEnvironmentManagers implements EnvironmentManagers { await manager.clearCache(); } } + + /** + * Sets the environment for a single scope, scope of undefined checks 'global'. + * If given an array of scopes, delegates to setEnvironments for batch setting. + */ + public async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + if (Array.isArray(scope)) { + return this.setEnvironments(scope, environment); + } + + const customScope = environment ? environment : scope; + const manager = this.getEnvironmentManager(customScope); + if (!manager) { + traceError( + `No environment manager found for scope: ${ + customScope instanceof Uri ? customScope.fsPath : customScope?.environmentPath?.fsPath + }`, + ); + + traceError(this.managers.map((m) => m.id).join(', ')); + return; + } + await manager.set(scope, environment); + + const project = scope ? this.pm.get(scope) : undefined; + if (scope) { + const packageManager = this.getPackageManager(environment); + if (project && packageManager) { + await setAllManagerSettings([ + { + project, + envManager: manager.id, + packageManager: packageManager.id, + }, + ]); + } + } + + const key = project ? project.uri.toString() : 'global'; + const oldEnv = this._previousEnvironments.get(key); + if (oldEnv?.envId.id !== environment?.envId.id) { + this._previousEnvironments.set(key, environment); + setImmediate(() => + this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: environment, old: oldEnv }), + ); + } + } + + /** + * Sets the given environment for the specified project URIs or globally. + * If a list of URIs is provided, sets the environment for each project; if 'global', sets it as the global environment. + */ + public async setEnvironments(scope: Uri[] | string, environment?: PythonEnvironment): Promise { + if (environment) { + const manager = this.managers.find((m) => m.id === environment.envId.managerId); + if (!manager) { + traceError( + `No environment manager found for [${environment.envId.managerId}]: ${ + environment.environmentPath ? environment.environmentPath.fsPath : '' + }`, + ); + traceError(this.managers.map((m) => m.id).join(', ')); + return; + } + + const promises: Promise[] = []; + const settings: EditAllManagerSettings[] = []; + const events: DidChangeEnvironmentEventArgs[] = []; + if (Array.isArray(scope) && scope.every((s) => s instanceof Uri)) { + promises.push(manager.set(scope, environment)); + scope.forEach((uri) => { + const m = this.getEnvironmentManager(uri); + if (manager.id !== m?.id) { + settings.push({ + project: this.pm.get(uri), + envManager: manager.id, + packageManager: manager.preferredPackageManagerId, + }); + } + + const project = this.pm.get(uri); + const key = project ? project.uri.toString() : 'global'; + const oldEnv = this._previousEnvironments.get(key); + if (oldEnv?.envId.id !== environment?.envId.id) { + this._previousEnvironments.set(key, environment); + events.push({ uri: project?.uri, new: environment, old: oldEnv }); + } + }); + } else if (typeof scope === 'string' && scope === 'global') { + const m = this.getEnvironmentManager(undefined); + promises.push(manager.set(undefined, environment)); + if (manager.id !== m?.id) { + settings.push({ + project: undefined, + envManager: manager.id, + packageManager: manager.preferredPackageManagerId, + }); + } + + const oldEnv = this._previousEnvironments.get('global'); + if (oldEnv?.envId.id !== environment?.envId.id) { + this._previousEnvironments.set('global', environment); + events.push({ uri: undefined, new: environment, old: oldEnv }); + } + } + await Promise.all(promises); + await setAllManagerSettings(settings); + setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e))); + } else { + const promises: Promise[] = []; + const events: DidChangeEnvironmentEventArgs[] = []; + if (Array.isArray(scope) && scope.every((s) => s instanceof Uri)) { + scope.forEach((uri) => { + const manager = this.getEnvironmentManager(uri); + if (manager) { + const setAndAddEvent = async () => { + await manager.set(uri); + + const project = this.pm.get(uri); + + // Always get the new first, then compare with the old. This has minor impact on the ordering of + // events. But it ensures that we always get the latest environment at the time of this call. + const newEnv = await manager.get(uri); + const key = project ? project.uri.toString() : 'global'; + const oldEnv = this._previousEnvironments.get(key); + if (oldEnv?.envId.id !== newEnv?.envId.id) { + this._previousEnvironments.set(key, newEnv); + events.push({ uri: project?.uri, new: newEnv, old: oldEnv }); + } + }; + promises.push(setAndAddEvent()); + } + }); + } else if (typeof scope === 'string' && scope === 'global') { + const manager = this.getEnvironmentManager(undefined); + if (manager) { + const setAndAddEvent = async () => { + await manager.set(undefined); + + // Always get the new first, then compare with the old. This has minor impact on the ordering of + // events. But it ensures that we always get the latest environment at the time of this call. + const newEnv = await manager.get(undefined); + const oldEnv = this._previousEnvironments.get('global'); + if (oldEnv?.envId.id !== newEnv?.envId.id) { + this._previousEnvironments.set('global', newEnv); + events.push({ uri: undefined, new: newEnv, old: oldEnv }); + } + }; + promises.push(setAndAddEvent()); + } + } + await Promise.all(promises); + setImmediate(() => events.forEach((e) => this._onDidChangeEnvironmentFiltered.fire(e))); + } + } + + /** + * Sets the environment for the given scopes, but only if the scope is not already set (i.e., is global or undefined). + * Existing environments for a scope are not overwritten. + * + */ + public async setEnvironmentsIfUnset(scope: Uri[] | string, environment?: PythonEnvironment): Promise { + if (!environment) { + return; + } + if (typeof scope === 'string' && scope === 'global') { + const current = await this.getEnvironment(undefined); + if (!current) { + await this.setEnvironments('global', environment); + } + } else if (Array.isArray(scope)) { + const urisToSet: Uri[] = []; + for (const uri of scope) { + const current = await this.getEnvironment(uri); + if (!current || current.envId.managerId === 'ms-python.python:system') { + // If the current environment is not set or is the system environment, set the new environment. + urisToSet.push(uri); + } + } + if (urisToSet.length > 0) { + await this.setEnvironments(urisToSet, environment); + } + } + } + + /** + * Gets the current Python environment for the given scope URI or undefined for 'global'. + * + * This method queries the appropriate environment manager for the latest environment for the scope. + * It also updates the internal cache and fires an event if the environment has changed since last check. + * + * @param scope The scope to get the environment. + * @returns The current PythonEnvironment for the scope, or undefined if none is set. + */ + async getEnvironment(scope: GetEnvironmentScope): Promise { + const manager = this.getEnvironmentManager(scope); + if (!manager) { + return undefined; + } + + const project = scope ? this.pm.get(scope) : undefined; + + // Always get the new first, then compare with the old. This has minor impact on the ordering of + // events. But it ensures that we always get the latest environment at the time of this call. + const newEnv = await manager.get(scope); + const key = project ? project.uri.toString() : 'global'; + const oldEnv = this._previousEnvironments.get(key); + if (oldEnv?.envId.id !== newEnv?.envId.id) { + this._previousEnvironments.set(key, newEnv); + setImmediate(() => + this._onDidChangeEnvironmentFiltered.fire({ uri: project?.uri, new: newEnv, old: oldEnv }), + ); + } + return newEnv; + } + + getProjectEnvManagers(uris: Uri[]): InternalEnvironmentManager[] { + const projectEnvManagers: InternalEnvironmentManager[] = []; + uris.forEach((uri) => { + const manager = this.getEnvironmentManager(uri); + if (manager && !projectEnvManagers.includes(manager)) { + projectEnvManagers.push(manager); + } + }); + return projectEnvManagers; + } } diff --git a/src/features/execution/activation.ts b/src/features/execution/activation.ts deleted file mode 100644 index b141ee5..0000000 --- a/src/features/execution/activation.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Terminal } from 'vscode'; -import { PythonCommandRunConfiguration, PythonEnvironment, TerminalShellType } from '../../api'; -import { identifyTerminalShell } from './shellDetector'; - -export function isActivatableEnvironment(environment: PythonEnvironment): boolean { - return !!environment.execInfo?.activation || !!environment.execInfo?.shellActivation; -} - -export function isActivatedRunAvailable(environment: PythonEnvironment): boolean { - return !!environment.execInfo?.activatedRun; -} - -export function getActivationCommand( - terminal: Terminal, - environment: PythonEnvironment, -): PythonCommandRunConfiguration[] | undefined { - const shell = identifyTerminalShell(terminal); - - let activation: PythonCommandRunConfiguration[] | undefined; - if (environment.execInfo?.shellActivation) { - activation = environment.execInfo.shellActivation.get(shell); - if (!activation) { - activation = environment.execInfo.shellActivation.get(TerminalShellType.unknown); - } - } - - if (!activation) { - activation = environment.execInfo?.activation; - } - - return activation; -} diff --git a/src/features/execution/envVarUtils.ts b/src/features/execution/envVarUtils.ts new file mode 100644 index 0000000..03c1e0c --- /dev/null +++ b/src/features/execution/envVarUtils.ts @@ -0,0 +1,34 @@ +import { Uri } from 'vscode'; +import { readFile } from '../../common/workspace.fs.apis'; +import { parse } from 'dotenv'; + +export function mergeEnvVariables( + base: { [key: string]: string | undefined }, + other: { [key: string]: string | undefined }, +) { + const env: { [key: string]: string | undefined } = {}; + + Object.keys(other).forEach((otherKey) => { + let value = other[otherKey]; + if (value === undefined || value === '') { + // SOME_ENV_VAR= + delete env[otherKey]; + } else { + Object.keys(base).forEach((baseKey) => { + const baseValue = base[baseKey]; + if (baseValue) { + value = value?.replace(`\${${baseKey}}`, baseValue); + } + }); + env[otherKey] = value; + } + }); + + return env; +} + +export async function parseEnvFile(envFile: Uri): Promise<{ [key: string]: string | undefined }> { + const raw = await readFile(envFile); + const contents = Buffer.from(raw).toString('utf-8'); + return parse(contents); +} diff --git a/src/features/execution/envVariableManager.ts b/src/features/execution/envVariableManager.ts new file mode 100644 index 0000000..d0317f2 --- /dev/null +++ b/src/features/execution/envVariableManager.ts @@ -0,0 +1,83 @@ +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { Event, EventEmitter, FileChangeType, Uri } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { DidChangeEnvironmentVariablesEventArgs, PythonEnvironmentVariablesApi } from '../../api'; +import { resolveVariables } from '../../common/utils/internalVariables'; +import { createFileSystemWatcher, getConfiguration } from '../../common/workspace.apis'; +import { PythonProjectManager } from '../../internal.api'; +import { mergeEnvVariables, parseEnvFile } from './envVarUtils'; + +export interface EnvVarManager extends PythonEnvironmentVariablesApi, Disposable {} + +export class PythonEnvVariableManager implements EnvVarManager { + private disposables: Disposable[] = []; + + private _onDidChangeEnvironmentVariables; + private watcher; + + constructor(private pm: PythonProjectManager) { + this._onDidChangeEnvironmentVariables = new EventEmitter(); + this.onDidChangeEnvironmentVariables = this._onDidChangeEnvironmentVariables.event; + + this.watcher = createFileSystemWatcher('**/.env'); + this.disposables.push( + this._onDidChangeEnvironmentVariables, + this.watcher, + this.watcher.onDidCreate((e) => + this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Created }), + ), + this.watcher.onDidChange((e) => + this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Changed }), + ), + this.watcher.onDidDelete((e) => + this._onDidChangeEnvironmentVariables.fire({ uri: e, changeType: FileChangeType.Deleted }), + ), + ); + } + + async getEnvironmentVariables( + uri: Uri | undefined, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }> { + const project = uri ? this.pm.get(uri) : undefined; + + const base = baseEnvVar || { ...process.env }; + let env = base; + + const config = getConfiguration('python', project?.uri ?? uri); + let envFilePath = config.get('envFile'); + envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath, uri)) : undefined; + + if (envFilePath && (await fsapi.pathExists(envFilePath))) { + const other = await parseEnvFile(Uri.file(envFilePath)); + env = mergeEnvVariables(env, other); + } + + let projectEnvFilePath = project ? path.normalize(path.join(project.uri.fsPath, '.env')) : undefined; + if ( + projectEnvFilePath && + projectEnvFilePath?.toLowerCase() !== envFilePath?.toLowerCase() && + (await fsapi.pathExists(projectEnvFilePath)) + ) { + const other = await parseEnvFile(Uri.file(projectEnvFilePath)); + env = mergeEnvVariables(env, other); + } + + if (overrides) { + for (const override of overrides) { + const other = override instanceof Uri ? await parseEnvFile(override) : override; + env = mergeEnvVariables(env, other); + } + } + + return env; + } + + onDidChangeEnvironmentVariables: Event; + + dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); + } +} diff --git a/src/features/execution/execUtils.ts b/src/features/execution/execUtils.ts index 9cdfb67..6804d85 100644 --- a/src/features/execution/execUtils.ts +++ b/src/features/execution/execUtils.ts @@ -1,10 +1,21 @@ -function quoteArg(arg: string): string { - if (arg.indexOf(' ') >= 0 && !(arg.startsWith('"') && arg.endsWith('"'))) { - return `"${arg}"`; +export function quoteStringIfNecessary(arg: string): string { + // Always return if already quoted to avoid double-quoting + if (arg.startsWith('"') && arg.endsWith('"')) { + return arg; } - return arg; + + // Don't quote single shell operators/special characters + if (arg.length === 1 && /[&|<>;()[\]{}$]/.test(arg)) { + return arg; + } + + // Quote if contains common shell special characters that are problematic across multiple shells + // Includes: space, &, |, <, >, ;, ', ", `, (, ), [, ], {, }, $ + const needsQuoting = /[\s&|<>;'"`()\[\]{}$]/.test(arg); + + return needsQuoting ? `"${arg}"` : arg; } export function quoteArgs(args: string[]): string[] { - return args.map(quoteArg); + return args.map(quoteStringIfNecessary); } diff --git a/src/features/execution/runAsTask.ts b/src/features/execution/runAsTask.ts index 39d90ed..6b7e14e 100644 --- a/src/features/execution/runAsTask.ts +++ b/src/features/execution/runAsTask.ts @@ -1,20 +1,42 @@ -import { ShellExecution, Task, TaskExecution, TaskPanelKind, TaskRevealKind, TaskScope, WorkspaceFolder } from 'vscode'; -import { PythonTaskExecutionOptions } from '../../internal.api'; -import { getWorkspaceFolder } from '../../common/workspace.apis'; -import { PythonEnvironment } from '../../api'; +import { + ShellExecution, + Task, + TaskExecution, + TaskPanelKind, + TaskRevealKind, + TaskScope, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { PythonEnvironment, PythonTaskExecutionOptions } from '../../api'; +import { traceInfo, traceWarn } from '../../common/logging'; import { executeTask } from '../../common/tasks.apis'; +import { getWorkspaceFolder } from '../../common/workspace.apis'; +import { quoteStringIfNecessary } from './execUtils'; + +function getWorkspaceFolderOrDefault(uri?: Uri): WorkspaceFolder | TaskScope { + const workspace = uri ? getWorkspaceFolder(uri) : undefined; + return workspace ?? TaskScope.Global; +} export async function runAsTask( - options: PythonTaskExecutionOptions, environment: PythonEnvironment, + options: PythonTaskExecutionOptions, extra?: { reveal?: TaskRevealKind }, ): Promise { - const workspace: WorkspaceFolder | TaskScope = getWorkspaceFolder(options.project.uri) ?? TaskScope.Global; + const workspace: WorkspaceFolder | TaskScope = getWorkspaceFolderOrDefault(options.project?.uri); + + let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable; + if (!executable) { + traceWarn('No Python executable found in environment; falling back to "python".'); + executable = 'python'; + } + // Check and quote the executable path if necessary + executable = quoteStringIfNecessary(executable); - const executable = - environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable ?? 'python'; const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; const allArgs = [...args, ...options.args]; + traceInfo(`Running as task: ${executable} ${allArgs.join(' ')}`); const task = new Task( { type: 'python' }, @@ -30,9 +52,8 @@ export async function runAsTask( echo: true, panel: TaskPanelKind.Shared, close: false, - showReuseMessage: false, + showReuseMessage: true, }; - const execution = await executeTask(task); - return execution; + return executeTask(task); } diff --git a/src/features/execution/runInBackground.ts b/src/features/execution/runInBackground.ts new file mode 100644 index 0000000..cf1ac8e --- /dev/null +++ b/src/features/execution/runInBackground.ts @@ -0,0 +1,71 @@ +import { PythonBackgroundRunOptions, PythonEnvironment, PythonProcess } from '../../api'; +import { spawnProcess } from '../../common/childProcess.apis'; +import { traceError, traceInfo, traceWarn } from '../../common/logging'; +import { quoteStringIfNecessary } from './execUtils'; + +export async function runInBackground( + environment: PythonEnvironment, + options: PythonBackgroundRunOptions, +): Promise { + let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable; + if (!executable) { + traceWarn('No Python executable found in environment; falling back to "python".'); + executable = 'python'; + } + + // Don't quote the executable path for spawn - it handles spaces correctly on its own + // Remove any existing quotes that might cause issues + // see https://github.com/nodejs/node/issues/7367 for more details on cp.spawn and quoting + if (executable.startsWith('"') && executable.endsWith('"')) { + executable = executable.substring(1, executable.length - 1); + } + + const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; + const allArgs = [...args, ...options.args]; + + // Log the command for debugging + traceInfo(`Running in background: "${executable}" ${allArgs.join(' ')}`); + + // Check if the file exists before trying to spawn it + try { + const fs = require('fs'); + if (!fs.existsSync(executable)) { + traceError( + `Python executable does not exist: ${executable}. Attempting to quote the path as a workaround...`, + ); + executable = quoteStringIfNecessary(executable); + } + } catch (err) { + traceWarn(`Error checking if executable exists: ${err instanceof Error ? err.message : String(err)}`); + } + + const proc = spawnProcess(executable, allArgs, { + stdio: 'pipe', + cwd: options.cwd, + env: options.env, + }); + + return { + pid: proc.pid, + stdin: proc.stdin, + stdout: proc.stdout, + stderr: proc.stderr, + kill: () => { + if (!proc.killed) { + proc.kill(); + } + }, + onExit: (listener: (code: number | null, signal: NodeJS.Signals | null, error?: Error | null) => void) => { + proc.on('exit', (code, signal) => { + if (code && code !== 0) { + traceError(`Process exited with error code: ${code}, signal: ${signal}`); + } + listener(code, signal, null); + }); + proc.on('error', (error) => { + traceError(`Process error: ${error?.message || error}${error?.stack ? '\n' + error.stack : ''}`); + listener(null, null, error); + }); + }, + }; +} diff --git a/src/features/execution/runInTerminal.ts b/src/features/execution/runInTerminal.ts deleted file mode 100644 index 7f247d9..0000000 --- a/src/features/execution/runInTerminal.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Terminal, TerminalShellExecution } from 'vscode'; -import { PythonEnvironment } from '../../api'; -import { PythonTerminalExecutionOptions } from '../../internal.api'; -import { getDedicatedTerminal, getProjectTerminal } from './terminal'; -import { onDidEndTerminalShellExecution } from '../../common/window.apis'; -import { createDeferred } from '../../common/utils/deferred'; -import { quoteArgs } from './execUtils'; - -export async function runInTerminal( - options: PythonTerminalExecutionOptions, - environment: PythonEnvironment, - extra?: { show?: boolean }, -): Promise { - let terminal: Terminal | undefined; - if (options.useDedicatedTerminal) { - terminal = await getDedicatedTerminal(options.useDedicatedTerminal, environment, options.project); - } else { - terminal = await getProjectTerminal(options.project, environment); - } - - if (terminal) { - if (extra?.show) { - terminal.show(); - } - - const executable = - environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable ?? 'python'; - const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; - const allArgs = [...args, ...options.args]; - - if (terminal.shellIntegration) { - let execution: TerminalShellExecution | undefined; - const deferred = createDeferred(); - const disposable = onDidEndTerminalShellExecution((e) => { - if (e.execution === execution) { - disposable.dispose(); - deferred.resolve(); - } - }); - execution = terminal.shellIntegration.executeCommand(executable, allArgs); - return deferred.promise; - } else { - const text = quoteArgs([executable, ...allArgs]).join(' '); - terminal.sendText(`${text}\n`); - } - } -} diff --git a/src/features/execution/terminal.ts b/src/features/execution/terminal.ts deleted file mode 100644 index 310f624..0000000 --- a/src/features/execution/terminal.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { - Disposable, - Progress, - ProgressLocation, - Terminal, - TerminalShellExecutionEndEvent, - TerminalShellIntegration, - Uri, - window, -} from 'vscode'; -import { PythonEnvironment, PythonProject } from '../../api'; -import * as path from 'path'; -import { - createTerminal, - onDidChangeTerminalShellIntegration, - onDidCloseTerminal, - onDidEndTerminalShellExecution, - onDidOpenTerminal, -} from '../../common/window.apis'; -import { getActivationCommand, isActivatableEnvironment } from './activation'; -import { createDeferred } from '../../common/utils/deferred'; -import { getConfiguration } from '../../common/workspace.apis'; -import { quoteArgs } from './execUtils'; - -const SHELL_INTEGRATION_TIMEOUT = 5; - -async function runActivationCommands( - shellIntegration: TerminalShellIntegration, - terminal: Terminal, - environment: PythonEnvironment, - progress: Progress<{ - message?: string; - increment?: number; - }>, -) { - const activationCommands = getActivationCommand(terminal, environment); - if (activationCommands) { - for (const command of activationCommands) { - const text = command.args ? `${command.executable} ${command.args.join(' ')}` : command.executable; - progress.report({ message: `Activating ${environment.displayName}: running ${text}` }); - const execPromise = createDeferred(); - const execution = command.args - ? shellIntegration.executeCommand(command.executable, command.args) - : shellIntegration.executeCommand(command.executable); - - const disposable = onDidEndTerminalShellExecution((e: TerminalShellExecutionEndEvent) => { - if (e.execution === execution) { - execPromise.resolve(); - disposable.dispose(); - } - }); - - await execPromise.promise; - } - } -} - -function runActivationCommandsLegacy(terminal: Terminal, environment: PythonEnvironment) { - const activationCommands = getActivationCommand(terminal, environment); - if (activationCommands) { - for (const command of activationCommands) { - const args = command.args ?? []; - const text = quoteArgs([command.executable, ...args]).join(' '); - terminal.sendText(text); - } - } -} - -async function activateEnvironmentOnCreation( - newTerminal: Terminal, - environment: PythonEnvironment, - progress: Progress<{ - message?: string; - increment?: number; - }>, -) { - const deferred = createDeferred(); - const disposables: Disposable[] = []; - let disposeTimer: Disposable | undefined; - - try { - let activated = false; - progress.report({ message: `Activating ${environment.displayName}: waiting for Shell Integration` }); - disposables.push( - onDidChangeTerminalShellIntegration(async ({ terminal, shellIntegration }) => { - if (terminal === newTerminal && !activated) { - disposeTimer?.dispose(); - activated = true; - await runActivationCommands(shellIntegration, terminal, environment, progress); - deferred.resolve(); - } - }), - onDidOpenTerminal((terminal) => { - if (terminal === newTerminal) { - let seconds = 0; - const timer = setInterval(() => { - if (newTerminal.shellIntegration || activated) { - return; - } - if (seconds >= SHELL_INTEGRATION_TIMEOUT) { - disposeTimer?.dispose(); - activated = true; - progress.report({ message: `Activating ${environment.displayName}: using legacy method` }); - runActivationCommandsLegacy(terminal, environment); - deferred.resolve(); - } else { - progress.report({ - message: `Activating ${environment.displayName}: waiting for Shell Integration ${ - SHELL_INTEGRATION_TIMEOUT - seconds - }s`, - }); - } - seconds++; - }, 1000); - - disposeTimer = new Disposable(() => { - clearInterval(timer); - disposeTimer = undefined; - }); - } - }), - onDidCloseTerminal((terminal) => { - if (terminal === newTerminal && !deferred.completed) { - deferred.reject(new Error('Terminal closed before activation')); - } - }), - new Disposable(() => { - disposeTimer?.dispose(); - }), - ); - await deferred.promise; - } finally { - disposables.forEach((d) => d.dispose()); - } -} - -export async function createPythonTerminal(environment: PythonEnvironment, cwd?: string | Uri): Promise { - const activatable = isActivatableEnvironment(environment); - const newTerminal = createTerminal({ - // name: `Python: ${environment.displayName}`, - iconPath: environment.iconPath, - cwd, - }); - - if (activatable) { - try { - await window.withProgress( - { - location: ProgressLocation.Window, - title: `Activating ${environment.displayName}`, - }, - async (progress) => { - await activateEnvironmentOnCreation(newTerminal, environment, progress); - }, - ); - } catch (e) { - window.showErrorMessage(`Failed to activate ${environment.displayName}`); - } - } - - return newTerminal; -} - -const dedicatedTerminals = new Map(); -export async function getDedicatedTerminal( - uri: Uri, - environment: PythonEnvironment, - project: PythonProject, - createNew: boolean = false, -): Promise { - const key = `${environment.envId.id}:${path.normalize(uri.fsPath)}`; - if (!createNew) { - const terminal = dedicatedTerminals.get(key); - if (terminal) { - return terminal; - } - } - - const config = getConfiguration('python', uri); - const cwd = config.get('terminal.executeInFileDir', false) ? path.dirname(uri.fsPath) : project.uri; - - const newTerminal = await createPythonTerminal(environment, cwd); - dedicatedTerminals.set(key, newTerminal); - - const disable = onDidCloseTerminal((terminal) => { - if (terminal === newTerminal) { - dedicatedTerminals.delete(key); - disable.dispose(); - } - }); - - return newTerminal; -} - -const projectTerminals = new Map(); -export async function getProjectTerminal( - project: PythonProject, - environment: PythonEnvironment, - createNew: boolean = false, -): Promise { - const key = `${environment.envId.id}:${path.normalize(project.uri.fsPath)}`; - if (!createNew) { - const terminal = projectTerminals.get(key); - if (terminal) { - return terminal; - } - } - - const newTerminal = await createPythonTerminal(environment, project.uri); - projectTerminals.set(key, newTerminal); - - const disable = onDidCloseTerminal((terminal) => { - if (terminal === newTerminal) { - projectTerminals.delete(key); - disable.dispose(); - } - }); - - return newTerminal; -} diff --git a/src/features/projectCreators.ts b/src/features/projectCreators.ts deleted file mode 100644 index 0c34de3..0000000 --- a/src/features/projectCreators.ts +++ /dev/null @@ -1,115 +0,0 @@ -import * as path from 'path'; -import { Disposable, Uri, window } from 'vscode'; -import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../api'; -import { ProjectCreators } from '../internal.api'; -import { showErrorMessage } from '../common/errors/utils'; -import { findFiles } from '../common/workspace.apis'; - -export class ProjectCreatorsImpl implements ProjectCreators { - private _creators: PythonProjectCreator[] = []; - - registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { - this._creators.push(creator); - return new Disposable(() => { - this._creators = this._creators.filter((item) => item !== creator); - }); - } - getProjectCreators(): PythonProjectCreator[] { - return this._creators; - } - - dispose() { - this._creators = []; - } -} - -export function registerExistingProjectProvider(pc: ProjectCreators): Disposable { - return pc.registerPythonProjectCreator({ - name: 'existingProjects', - displayName: 'Add Existing Projects', - - async create(_options?: PythonProjectCreatorOptions): Promise { - const results = await window.showOpenDialog({ - canSelectFiles: true, - canSelectFolders: true, - canSelectMany: true, - filters: { - python: ['py'], - }, - title: 'Select a file(s) or folder(s) to add as Python projects', - }); - - if (!results || results.length === 0) { - return; - } - - return results.map((r) => ({ - name: path.basename(r.fsPath), - uri: r, - })); - }, - }); -} - -function getUniqueUri(uris: Uri[]): { - label: string; - description: string; - uri: Uri; -}[] { - const files = uris.map((uri) => uri.fsPath).sort(); - const dirs: Map = new Map(); - files.forEach((file) => { - const dir = path.dirname(file); - if (dirs.has(dir)) { - return; - } - dirs.set(dir, file); - }); - return Array.from(dirs.entries()) - .map(([dir, file]) => ({ - label: path.basename(dir), - description: file, - uri: Uri.file(dir), - })) - .sort((a, b) => a.label.localeCompare(b.label)); -} - -async function pickProjects(uris: Uri[]): Promise { - const items = getUniqueUri(uris); - - const selected = await window.showQuickPick(items, { - canPickMany: true, - ignoreFocusOut: true, - placeHolder: 'Select the folders to add as Python projects', - }); - - return selected?.map((s) => s.uri); -} - -export function registerAutoProjectProvider(pc: ProjectCreators): Disposable { - return pc.registerPythonProjectCreator({ - name: 'autoProjects', - displayName: 'Auto Find Projects', - description: 'Automatically find folders with `pyproject.toml` or `setup.py` files.', - - async create(_options?: PythonProjectCreatorOptions): Promise { - const files = await findFiles('**/{pyproject.toml,setup.py}'); - if (!files || files.length === 0) { - setImmediate(() => { - showErrorMessage('No projects found'); - }); - return; - } - - const projects = await pickProjects(files); - if (!projects || projects.length === 0) { - return; - } - - return projects.map((uri) => ({ - name: path.basename(uri.fsPath), - uri, - })); - }, - }); -} diff --git a/src/features/projectManager.ts b/src/features/projectManager.ts index 31d354f..733278a 100644 --- a/src/features/projectManager.ts +++ b/src/features/projectManager.ts @@ -1,14 +1,21 @@ -import { Uri, EventEmitter, MarkdownString, Disposable } from 'vscode'; -import { IconPath, PythonProject } from '../api'; import * as path from 'path'; -import { PythonProjectManager, PythonProjectSettings, PythonProjectsImpl } from '../internal.api'; +import { Disposable, EventEmitter, MarkdownString, Uri, workspace } from 'vscode'; +import { IconPath, PythonProject } from '../api'; +import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../common/constants'; +import { createSimpleDebounce } from '../common/utils/debounce'; import { getConfiguration, getWorkspaceFolders, onDidChangeConfiguration, onDidChangeWorkspaceFolders, } from '../common/workspace.apis'; -import { createSimpleDebounce } from '../common/utils/debounce'; +import { PythonProjectManager, PythonProjectSettings, PythonProjectsImpl } from '../internal.api'; +import { + addPythonProjectSetting, + EditProjectSettings, + getDefaultEnvManagerSetting, + getDefaultPkgManagerSetting, +} from './settings/settingHelpers'; type ProjectArray = PythonProject[]; @@ -17,6 +24,8 @@ export class PythonProjectManagerImpl implements PythonProjectManager { private _projects = new Map(); private readonly _onDidChangeProjects = new EventEmitter(); public readonly onDidChangeProjects = this._onDidChangeProjects.event; + + // Debounce the updateProjects method to avoid excessive update calls private readonly updateDebounce = createSimpleDebounce(100, () => this.updateProjects()); initialize(): void { @@ -39,41 +48,64 @@ export class PythonProjectManagerImpl implements PythonProjectManager { ); } + /** + * + * Gathers the projects which are configured in settings and all workspace roots. + * @returns An array of PythonProject objects representing the initial projects. + */ private getInitialProjects(): ProjectArray { const newProjects: ProjectArray = []; const workspaces = getWorkspaceFolders() ?? []; for (const w of workspaces) { const config = getConfiguration('python-envs', w.uri); const overrides = config.get('pythonProjects', []); - newProjects.push(new PythonProjectsImpl(w.name, w.uri)); + + // Add the workspace root as a project if not already present + if (!newProjects.some((p) => p.uri.toString() === w.uri.toString())) { + newProjects.push(new PythonProjectsImpl(w.name, w.uri)); + } + + // For each override, resolve its path and add as a project if not already present for (const o of overrides) { - const uri = Uri.file(path.resolve(w.uri.fsPath, o.path)); - newProjects.push(new PythonProjectsImpl(o.path, uri)); + let uriFromWorkspace: Uri | undefined = undefined; + // if override has a workspace property, resolve the path relative to that workspace + if (o.workspace) { + // + const workspaceFolder = workspaces.find((ws) => ws.name === o.workspace); + if (workspaceFolder) { + if (workspaceFolder.uri.toString() !== w.uri.toString()) { + continue; // skip if the workspace is not the same as the current workspace + } + uriFromWorkspace = Uri.file(path.resolve(workspaceFolder.uri.fsPath, o.path)); + } + } + const uri = uriFromWorkspace ? uriFromWorkspace : Uri.file(path.resolve(w.uri.fsPath, o.path)); + + // Check if the project already exists in the newProjects array + if (!newProjects.some((p) => p.uri.toString() === uri.toString())) { + newProjects.push(new PythonProjectsImpl(o.path, uri)); + } } } return newProjects; } + /** + * Get initial projects from the workspace(s) config settings + * then updates the internal _projects map to reflect the current state and + * fires the onDidChangeProjects event if there are any changes. + */ private updateProjects(): void { - const workspaces = getWorkspaceFolders() ?? []; + const newProjects: ProjectArray = this.getInitialProjects(); const existingProjects = Array.from(this._projects.values()); - const newProjects: ProjectArray = []; - - for (const w of workspaces) { - const config = getConfiguration('python-envs', w.uri); - const overrides = config.get('pythonProjects', []); - newProjects.push(new PythonProjectsImpl(w.name, w.uri)); - for (const o of overrides) { - const uri = Uri.file(path.resolve(w.uri.fsPath, o.path)); - newProjects.push(new PythonProjectsImpl(o.path, uri)); - } - } + // Remove projects that are no longer in the workspace settings const projectsToRemove = existingProjects.filter( (w) => !newProjects.find((n) => n.uri.toString() === w.uri.toString()), ); projectsToRemove.forEach((w) => this._projects.delete(w.uri.toString())); + // Add new projects that are in the workspace settings but not in the existing projects const projectsToAdd = newProjects.filter( (n) => !existingProjects.find((w) => w.uri.toString() === n.uri.toString()), ); @@ -92,14 +124,40 @@ export class PythonProjectManagerImpl implements PythonProjectManager { return new PythonProjectsImpl(name, uri, options); } - add(projects: PythonProject | ProjectArray): void { + async add(projects: PythonProject | ProjectArray): Promise { const _projects = Array.isArray(projects) ? projects : [projects]; if (_projects.length === 0) { return; } - - _projects.forEach((w) => this._projects.set(w.uri.toString(), w)); + const edits: EditProjectSettings[] = []; + + const envManagerId = getDefaultEnvManagerSetting(this); + const pkgManagerId = getDefaultPkgManagerSetting(this); + + const globalConfig = workspace.getConfiguration('python-envs', undefined); + const defaultEnvManager = globalConfig.get('defaultEnvManager', DEFAULT_ENV_MANAGER_ID); + const defaultPkgManager = globalConfig.get('defaultPackageManager', DEFAULT_PACKAGE_MANAGER_ID); + + _projects.forEach((currProject) => { + const workspaces = getWorkspaceFolders() ?? []; + const isRoot = workspaces.some((w) => w.uri.toString() === currProject.uri.toString()); + if (isRoot) { + // for root projects, add setting if not default + if (envManagerId !== defaultEnvManager || pkgManagerId !== defaultPkgManager) { + edits.push({ project: currProject, envManager: envManagerId, packageManager: pkgManagerId }); + } + } else { + // for non-root projects, always add setting + edits.push({ project: currProject, envManager: envManagerId, packageManager: pkgManagerId }); + } + // handles adding the project to this._projects map + return this._projects.set(currProject.uri.toString(), currProject); + }); this._onDidChangeProjects.fire(Array.from(this._projects.values())); + + if (edits.length > 0) { + await addPythonProjectSetting(edits); + } } remove(projects: PythonProject | ProjectArray): void { @@ -112,8 +170,19 @@ export class PythonProjectManagerImpl implements PythonProjectManager { this._onDidChangeProjects.fire(Array.from(this._projects.values())); } - getProjects(): ReadonlyArray { - return Array.from(this._projects.values()); + getProjects(uris?: Uri[]): ReadonlyArray { + if (uris === undefined) { + return Array.from(this._projects.values()); + } else { + const projects: PythonProject[] = []; + for (const uri of uris) { + const project = this.get(uri); + if (project !== undefined && !projects.includes(project)) { + projects.push(project); + } + } + return projects; + } } get(uri: Uri): PythonProject | undefined { @@ -124,6 +193,11 @@ export class PythonProjectManagerImpl implements PythonProjectManager { return pythonProject; } + /** + * Finds the single project that matches the given URI if it exists. + * @param uri The URI of the project to find. + * @returns The project with the given URI, or undefined if not found. + */ private findProjectByUri(uri: Uri): PythonProject | undefined { const _projects = Array.from(this._projects.values()).sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length); @@ -137,6 +211,13 @@ export class PythonProjectManagerImpl implements PythonProjectManager { return undefined; } + /** + * Checks if a given file or folder path (normalizedUriPath) + * is the same as, or is inside, a project path + * @normalizedProjectPath Project path to check against. + * @normalizedUriPath File or folder path to check. + * @returns true if the file or folder path is the same as or inside the project path, false otherwise. + */ private isUriMatching(normalizedUriPath: string, normalizedProjectPath: string): boolean { if (normalizedProjectPath === normalizedUriPath) { return true; diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index f3250de..cf1e048 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -1,4 +1,4 @@ -import { Uri, Disposable, Event, EventEmitter } from 'vscode'; +import { Uri, Disposable, Event, EventEmitter, Terminal, TaskExecution } from 'vscode'; import { PythonEnvironmentApi, PythonEnvironment, @@ -21,29 +21,66 @@ import { PackageId, PythonProjectCreator, ResolveEnvironmentContext, - PackageInstallOptions, + PackageManagementOptions, + PythonProcess, + PythonTaskExecutionOptions, + PythonTerminalExecutionOptions, + PythonBackgroundRunOptions, + PythonTerminalCreateOptions, + DidChangeEnvironmentVariablesEventArgs, + CreateEnvironmentOptions, } from '../api'; import { EnvironmentManagers, + InternalEnvironmentManager, ProjectCreators, PythonEnvironmentImpl, PythonPackageImpl, PythonProjectManager, } from '../internal.api'; import { createDeferred } from '../common/utils/deferred'; -import { traceError } from '../common/logging'; -import { showErrorMessage } from '../common/errors/utils'; +import { traceInfo } from '../common/logging'; +import { pickEnvironmentManager } from '../common/pickers/managers'; +import { handlePythonPath } from '../common/utils/pythonPath'; +import { TerminalManager } from './terminal/terminalManager'; +import { runAsTask } from './execution/runAsTask'; +import { runInTerminal } from './terminal/runInTerminal'; +import { runInBackground } from './execution/runInBackground'; +import { EnvVarManager } from './execution/envVariableManager'; +import { checkUri } from '../common/utils/pathUtils'; +import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady'; class PythonEnvironmentApiImpl implements PythonEnvironmentApi { private readonly _onDidChangeEnvironments = new EventEmitter(); private readonly _onDidChangeEnvironment = new EventEmitter(); private readonly _onDidChangePythonProjects = new EventEmitter(); private readonly _onDidChangePackages = new EventEmitter(); + private readonly _onDidChangeEnvironmentVariables = new EventEmitter(); + constructor( private readonly envManagers: EnvironmentManagers, private readonly projectManager: PythonProjectManager, private readonly projectCreators: ProjectCreators, - ) {} + private readonly terminalManager: TerminalManager, + private readonly envVarManager: EnvVarManager, + private readonly disposables: Disposable[] = [], + ) { + this.disposables.push( + this._onDidChangeEnvironment, + this._onDidChangeEnvironments, + this._onDidChangePythonProjects, + this._onDidChangePackages, + this._onDidChangeEnvironmentVariables, + this.envManagers.onDidChangeEnvironmentFiltered((e) => { + this._onDidChangeEnvironment.fire(e); + const location = e.uri?.fsPath ?? 'global'; + traceInfo( + `Python API: Changed environment from ${e.old?.displayName} to ${e.new?.displayName} for: ${location}`, + ); + }), + this.envVarManager.onDidChangeEnvironmentVariables((e) => this._onDidChangeEnvironmentVariables.fire(e)), + ); + } registerEnvironmentManager(manager: EnvironmentManager): Disposable { const disposables: Disposable[] = []; @@ -54,14 +91,15 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { if (manager.onDidChangeEnvironment) { disposables.push( manager.onDidChangeEnvironment((e) => { - const mgr = this.envManagers.getEnvironmentManager(e.uri); - if (mgr?.equals(manager)) { - // Fire this event only if the manager set for current uri - // is the same as the manager that triggered environment change - setImmediate(() => { - this._onDidChangeEnvironment.fire(e); - }); - } + setImmediate(async () => { + // This will ensure that we use the right manager and only trigger the event + // if the user selected manager decided to change the environment. + // This ensures that if a unselected manager changes environment and raises events + // we don't trigger the Python API event which can cause issues with the consumers. + // This will trigger onDidChangeEnvironmentFiltered event in envManagers, which the Python + // API listens to, and re-triggers the onDidChangeEnvironment event. + await this.envManagers.getEnvironment(e.uri); + }); }), ); } @@ -80,14 +118,53 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { }; return new PythonEnvironmentImpl(envId, info); } - createEnvironment(scope: CreateEnvironmentScope): Promise { - const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope.uri); - if (!manager) { - return Promise.reject(new Error('No environment manager found')); + + async createEnvironment( + scope: CreateEnvironmentScope, + options: CreateEnvironmentOptions | undefined, + ): Promise { + if (scope === 'global' || (!Array.isArray(scope) && scope instanceof Uri)) { + await waitForEnvManager(scope === 'global' ? undefined : [scope]); + const manager = this.envManagers.getEnvironmentManager(scope === 'global' ? undefined : scope); + if (!manager) { + throw new Error('No environment manager found'); + } + if (!manager.supportsCreate) { + throw new Error(`Environment manager does not support creating environments: ${manager.id}`); + } + return manager.create(scope, options); + } else if (Array.isArray(scope) && scope.length === 1 && scope[0] instanceof Uri) { + return this.createEnvironment(scope[0], options); + } else if (Array.isArray(scope) && scope.length > 0 && scope.every((s) => s instanceof Uri)) { + await waitForEnvManager(scope); + const managers: InternalEnvironmentManager[] = []; + scope.forEach((s) => { + const manager = this.envManagers.getEnvironmentManager(s); + if (manager && !managers.includes(manager) && manager.supportsCreate) { + managers.push(manager); + } + }); + + if (managers.length === 0) { + throw new Error('No environment managers found'); + } + + const managerId = await pickEnvironmentManager(managers); + if (!managerId) { + throw new Error('No environment manager selected'); + } + + const manager = managers.find((m) => m.id === managerId); + if (!manager) { + throw new Error('No environment manager found'); + } + + const result = await manager.create(scope, options); + return result; } - return manager.create(scope); } - removeEnvironment(environment: PythonEnvironment): Promise { + async removeEnvironment(environment: PythonEnvironment): Promise { + await waitForEnvManagerId([environment.envId.managerId]); const manager = this.envManagers.getEnvironmentManager(environment); if (!manager) { return Promise.reject(new Error('No environment manager found')); @@ -95,65 +172,65 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { return manager.remove(environment); } async refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { - if (scope === undefined) { - await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(scope))); + const currentScope = checkUri(scope) as RefreshEnvironmentsScope; + + if (currentScope === undefined) { + await waitForAllEnvManagers(); + await Promise.all(this.envManagers.managers.map((manager) => manager.refresh(currentScope))); return Promise.resolve(); } - const manager = this.envManagers.getEnvironmentManager(scope); + + await waitForEnvManager([currentScope]); + const manager = this.envManagers.getEnvironmentManager(currentScope); if (!manager) { - return Promise.reject(new Error(`No environment manager found for: ${scope.fsPath}`)); + return Promise.reject(new Error(`No environment manager found for: ${currentScope.fsPath}`)); } - return manager.refresh(scope); + return manager.refresh(currentScope); } async getEnvironments(scope: GetEnvironmentsScope): Promise { - if (scope === 'all' || scope === 'global') { - const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(scope)); + const currentScope = checkUri(scope) as GetEnvironmentsScope; + if (currentScope === 'all' || currentScope === 'global') { + await waitForAllEnvManagers(); + const promises = this.envManagers.managers.map((manager) => manager.getEnvironments(currentScope)); const items = await Promise.all(promises); return items.flat(); } - const manager = this.envManagers.getEnvironmentManager(scope); + + await waitForEnvManager([currentScope]); + const manager = this.envManagers.getEnvironmentManager(currentScope); if (!manager) { return []; } - const items = await manager.getEnvironments(scope); + const items = await manager.getEnvironments(currentScope); return items; } onDidChangeEnvironments: Event = this._onDidChangeEnvironments.event; - setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): void { - const manager = this.envManagers.getEnvironmentManager(scope); - if (!manager) { - throw new Error('No environment manager found'); - } - manager.set(scope, environment); + async setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + const currentScope = checkUri(scope) as SetEnvironmentScope; + await waitForEnvManager( + currentScope ? (currentScope instanceof Uri ? [currentScope] : currentScope) : undefined, + ); + return this.envManagers.setEnvironment(currentScope, environment); } - async getEnvironment(context: GetEnvironmentScope): Promise { - const manager = this.envManagers.getEnvironmentManager(context); - if (!manager) { - return undefined; - } - return await manager.get(context); + async getEnvironment(scope: GetEnvironmentScope): Promise { + const currentScope = checkUri(scope) as GetEnvironmentScope; + await waitForEnvManager(currentScope ? [currentScope] : undefined); + return this.envManagers.getEnvironment(currentScope); } onDidChangeEnvironment: Event = this._onDidChangeEnvironment.event; async resolveEnvironment(context: ResolveEnvironmentContext): Promise { - const manager = this.envManagers.getEnvironmentManager(context); - if (!manager) { - const data = context instanceof Uri ? context.fsPath : context.environmentPath.fsPath; - traceError(`No environment manager found: ${data}`); - traceError(`Know environment managers: ${this.envManagers.managers.map((m) => m.name).join(', ')}`); - showErrorMessage('No environment manager found'); - return undefined; - } - const env = await manager.resolve(context); - if (env && !env.execInfo) { - traceError(`Environment wasn't resolved correctly, missing execution info: ${env.name}`); - traceError(`Environment: ${JSON.stringify(env)}`); - traceError(`Resolved by: ${manager.id}`); - showErrorMessage("Environment wasn't resolved correctly, missing execution info"); - return undefined; - } + await waitForAllEnvManagers(); + const projects = this.projectManager.getProjects(); + const projectEnvManagers: InternalEnvironmentManager[] = []; + projects.forEach((p) => { + const manager = this.envManagers.getEnvironmentManager(p.uri); + if (manager && !projectEnvManagers.includes(manager)) { + projectEnvManagers.push(manager); + } + }); - return env; + return await handlePythonPath(context, this.envManagers.managers, projectEnvManagers); } registerPackageManager(manager: PackageManager): Disposable { @@ -164,28 +241,24 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } return new Disposable(() => disposables.forEach((d) => d.dispose())); } - installPackages(context: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise { - const manager = this.envManagers.getPackageManager(context); - if (!manager) { - return Promise.reject(new Error('No package manager found')); - } - return manager.install(context, packages, options); - } - uninstallPackages(context: PythonEnvironment, packages: Package[] | string[]): Promise { + async managePackages(context: PythonEnvironment, options: PackageManagementOptions): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); } - return manager.uninstall(context, packages); + return manager.manage(context, options); } - refreshPackages(context: PythonEnvironment): Promise { + async refreshPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.reject(new Error('No package manager found')); } return manager.refresh(context); } - getPackages(context: PythonEnvironment): Promise { + async getPackages(context: PythonEnvironment): Promise { + await waitForEnvManagerId([context.envId.managerId]); const manager = this.envManagers.getPackageManager(context); if (!manager) { return Promise.resolve(undefined); @@ -218,11 +291,51 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { } onDidChangePythonProjects: Event = this._onDidChangePythonProjects.event; getPythonProject(uri: Uri): PythonProject | undefined { - return this.projectManager.get(uri); + return this.projectManager.get(checkUri(uri) as Uri); } registerPythonProjectCreator(creator: PythonProjectCreator): Disposable { return this.projectCreators.registerPythonProjectCreator(creator); } + async createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise { + return this.terminalManager.create(environment, options); + } + async runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise { + const terminal = await this.terminalManager.getProjectTerminal( + options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), + environment, + ); + await runInTerminal(environment, terminal, options); + return terminal; + } + async runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise { + const terminal = await this.terminalManager.getDedicatedTerminal( + terminalKey, + options.cwd instanceof Uri ? options.cwd : Uri.file(options.cwd), + environment, + ); + await runInTerminal(environment, terminal, options); + return Promise.resolve(terminal); + } + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise { + return runAsTask(environment, options); + } + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise { + return runInBackground(environment, options); + } + + onDidChangeEnvironmentVariables: Event = + this._onDidChangeEnvironmentVariables.event; + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }> { + return this.envVarManager.getEnvironmentVariables(checkUri(uri) as Uri, overrides, baseEnvVar); + } } let _deferred = createDeferred(); @@ -230,8 +343,12 @@ export function setPythonApi( envMgr: EnvironmentManagers, projectMgr: PythonProjectManager, projectCreators: ProjectCreators, + terminalManager: TerminalManager, + envVarManager: EnvVarManager, ) { - _deferred.resolve(new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators)); + _deferred.resolve( + new PythonEnvironmentApiImpl(envMgr, projectMgr, projectCreators, terminalManager, envVarManager), + ); } export function getPythonApi(): Promise { diff --git a/src/features/settings/settingHelpers.ts b/src/features/settings/settingHelpers.ts index 8e23a38..bad5536 100644 --- a/src/features/settings/settingHelpers.ts +++ b/src/features/settings/settingHelpers.ts @@ -1,9 +1,17 @@ import * as path from 'path'; -import { ConfigurationScope, ConfigurationTarget, Uri, workspace, WorkspaceConfiguration } from 'vscode'; -import { PythonProjectManager, PythonProjectSettings } from '../../internal.api'; -import { traceError, traceInfo } from '../../common/logging'; +import { + ConfigurationScope, + ConfigurationTarget, + Uri, + workspace, + WorkspaceConfiguration, + WorkspaceFolder, +} from 'vscode'; import { PythonProject } from '../../api'; import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../../common/constants'; +import { traceError, traceInfo, traceWarn } from '../../common/logging'; +import { getConfiguration, getWorkspaceFile, getWorkspaceFolders } from '../../common/workspace.apis'; +import { PythonProjectManager, PythonProjectSettings } from '../../internal.api'; function getSettings( wm: PythonProjectManager, @@ -23,17 +31,34 @@ function getSettings( return undefined; } +let DEFAULT_ENV_MANAGER_BROKEN = false; +let hasShownDefaultEnvManagerBrokenWarn = false; + +export function setDefaultEnvManagerBroken(broken: boolean) { + DEFAULT_ENV_MANAGER_BROKEN = broken; +} +export function isDefaultEnvManagerBroken(): boolean { + return DEFAULT_ENV_MANAGER_BROKEN; +} + export function getDefaultEnvManagerSetting(wm: PythonProjectManager, scope?: Uri): string { const config = workspace.getConfiguration('python-envs', scope); const settings = getSettings(wm, config, scope); if (settings && settings.envManager.length > 0) { return settings.envManager; } - + // Only show the warning once per session + if (isDefaultEnvManagerBroken()) { + if (!hasShownDefaultEnvManagerBrokenWarn) { + traceWarn(`Default environment manager is broken, using system default: ${DEFAULT_ENV_MANAGER_ID}`); + hasShownDefaultEnvManagerBrokenWarn = true; + } + return DEFAULT_ENV_MANAGER_ID; + } const defaultManager = config.get('defaultEnvManager'); if (defaultManager === undefined || defaultManager === null || defaultManager === '') { traceError('No default environment manager set. Check setting python-envs.defaultEnvManager'); - traceInfo(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`); + traceWarn(`Using system default package manager: ${DEFAULT_ENV_MANAGER_ID}`); return DEFAULT_ENV_MANAGER_ID; } return defaultManager; @@ -63,81 +88,382 @@ export function getDefaultPkgManagerSetting( return defaultManager; } -export async function setEnvironmentManager(context: Uri, managerId: string, wm: PythonProjectManager): Promise { - const pw = wm.get(context); - const w = workspace.getWorkspaceFolder(context); - if (pw && w) { - const config = workspace.getConfiguration('python-envs', pw.uri); +export interface EditAllManagerSettings { + // undefined means global + project?: PythonProject; + envManager: string; + packageManager: string; +} +interface EditAllManagerSettingsInternal { + project: PythonProject; + envManager: string; + packageManager: string; +} +export async function setAllManagerSettings(edits: EditAllManagerSettings[]): Promise { + const noWorkspace: EditAllManagerSettingsInternal[] = []; + const workspaces = new Map(); + edits + .filter((e) => !!e.project) + .map((e) => e as EditAllManagerSettingsInternal) + .forEach((e) => { + const w = workspace.getWorkspaceFolder(e.project.uri); + if (w) { + workspaces.set(w, [ + ...(workspaces.get(w) || []), + { project: e.project, envManager: e.envManager, packageManager: e.packageManager }, + ]); + } else { + noWorkspace.push({ project: e.project, envManager: e.envManager, packageManager: e.packageManager }); + } + }); + + noWorkspace.forEach((e) => { + if (e.project) { + traceInfo(`Unable to find workspace for ${e.project.uri.fsPath}, will use global settings for this.`); + } + }); + + const workspaceFile = getWorkspaceFile(); + const promises: Thenable[] = []; + + workspaces.forEach((es, w) => { + const config = workspace.getConfiguration('python-envs', w); const overrides = config.get('pythonProjects', []); - const pwPath = path.normalize(pw.uri.fsPath); - const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); - if (index >= 0) { - overrides[index].envManager = managerId; - await config.update('pythonProjects', overrides, ConfigurationTarget.Workspace); - } else { - await config.update('defaultEnvManager', managerId, ConfigurationTarget.Workspace); + es.forEach((e) => { + const pwPath = path.normalize(e.project.uri.fsPath); + const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); + if (index >= 0) { + overrides[index].envManager = e.envManager; + overrides[index].packageManager = e.packageManager; + } else if (workspaceFile) { + overrides.push({ + path: path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'), + envManager: e.envManager, + packageManager: e.packageManager, + }); + } else { + if (config.get('defaultEnvManager') !== e.envManager) { + promises.push(config.update('defaultEnvManager', e.envManager, ConfigurationTarget.Workspace)); + } + if (config.get('defaultPackageManager') !== e.packageManager) { + promises.push( + config.update('defaultPackageManager', e.packageManager, ConfigurationTarget.Workspace), + ); + } + } + }); + promises.push( + config.update( + 'pythonProjects', + overrides, + workspaceFile ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace, + ), + ); + }); + + const config = workspace.getConfiguration('python-envs', undefined); + edits + .filter((e) => !e.project) + .forEach((e) => { + if (config.get('defaultEnvManager') !== e.envManager) { + promises.push(config.update('defaultEnvManager', e.envManager, ConfigurationTarget.Global)); + } + if (config.get('defaultPackageManager') !== e.packageManager) { + promises.push(config.update('defaultPackageManager', e.packageManager, ConfigurationTarget.Global)); + } + }); + + await Promise.all(promises); +} + +export interface EditEnvManagerSettings { + // undefined means global + project?: PythonProject; + envManager: string; +} +interface EditEnvManagerSettingsInternal { + project: PythonProject; + envManager: string; +} +export async function setEnvironmentManager(edits: EditEnvManagerSettings[]): Promise { + const noWorkspace: EditEnvManagerSettingsInternal[] = []; + const workspaces = new Map(); + edits + .filter((e) => !!e.project) + .map((e) => e as EditEnvManagerSettingsInternal) + .forEach((e) => { + const w = workspace.getWorkspaceFolder(e.project.uri); + if (w) { + workspaces.set(w, [...(workspaces.get(w) || []), { project: e.project, envManager: e.envManager }]); + } else { + noWorkspace.push({ project: e.project, envManager: e.envManager }); + } + }); + + noWorkspace.forEach((e) => { + if (e.project) { + traceError(`Unable to find workspace for ${e.project.uri.fsPath}`); } - } else { - const config = workspace.getConfiguration('python-envs', undefined); - await config.update('defaultEnvManager', managerId, ConfigurationTarget.Global); - } + }); + + const promises: Thenable[] = []; + + workspaces.forEach((es, w) => { + const config = workspace.getConfiguration('python-envs', w.uri); + const overrides = config.get('pythonProjects', []); + es.forEach((e) => { + const pwPath = path.normalize(e.project.uri.fsPath); + const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); + if (index >= 0) { + overrides[index].envManager = e.envManager; + } else if (config.get('defaultEnvManager') !== e.envManager) { + promises.push(config.update('defaultEnvManager', e.envManager, ConfigurationTarget.Workspace)); + } + }); + promises.push(config.update('pythonProjects', overrides, ConfigurationTarget.Workspace)); + }); + + const config = workspace.getConfiguration('python-envs', undefined); + edits + .filter((e) => !e.project) + .forEach((e) => { + if (config.get('defaultEnvManager') !== e.envManager) { + promises.push(config.update('defaultEnvManager', e.envManager, ConfigurationTarget.Global)); + } + }); + + await Promise.all(promises); +} + +export interface EditPackageManagerSettings { + // undefined means global + project?: PythonProject; + packageManager: string; +} +interface EditPackageManagerSettingsInternal { + project: PythonProject; + packageManager: string; } +export async function setPackageManager(edits: EditPackageManagerSettings[]): Promise { + const noWorkspace: EditPackageManagerSettingsInternal[] = []; + const workspaces = new Map(); + edits + .filter((e) => !!e.project) + .map((e) => e as EditPackageManagerSettingsInternal) + .forEach((e) => { + const w = workspace.getWorkspaceFolder(e.project.uri); + if (w) { + workspaces.set(w, [ + ...(workspaces.get(w) || []), + { project: e.project, packageManager: e.packageManager }, + ]); + } else { + noWorkspace.push({ project: e.project, packageManager: e.packageManager }); + } + }); -export async function setPackageManager(context: Uri, managerId: string, wm: PythonProjectManager): Promise { - const pw = wm.get(context); - const w = workspace.getWorkspaceFolder(context); - if (pw && w) { - const config = workspace.getConfiguration('python-envs', pw.uri); + noWorkspace.forEach((e) => { + if (e.project) { + traceError(`Unable to find workspace for ${e.project.uri.fsPath}`); + } + }); + + const promises: Thenable[] = []; + + workspaces.forEach((es, w) => { + const config = workspace.getConfiguration('python-envs', w.uri); const overrides = config.get('pythonProjects', []); - const pwPath = path.normalize(pw.uri.fsPath); - const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); - if (index >= 0) { - overrides[index].packageManager = managerId; - await config.update('pythonProjects', overrides, ConfigurationTarget.Workspace); + es.forEach((e) => { + const pwPath = path.normalize(e.project.uri.fsPath); + const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); + if (index >= 0) { + overrides[index].packageManager = e.packageManager; + } else if (config.get('defaultPackageManager') !== e.packageManager) { + promises.push(config.update('defaultPackageManager', e.packageManager, ConfigurationTarget.Workspace)); + } + }); + promises.push(config.update('pythonProjects', overrides, ConfigurationTarget.Workspace)); + }); + + const config = workspace.getConfiguration('python-envs', undefined); + edits + .filter((e) => !e.project) + .forEach((e) => { + if (config.get('defaultPackageManager') !== e.packageManager) { + promises.push(config.update('defaultPackageManager', e.packageManager, ConfigurationTarget.Global)); + } + }); + + await Promise.all(promises); +} + +export interface EditProjectSettings { + project: PythonProject; + envManager?: string; + packageManager?: string; + workspace?: string; +} + +export async function addPythonProjectSetting(edits: EditProjectSettings[]): Promise { + const noWorkspace: EditProjectSettings[] = []; + const workspaces = new Map(); + const globalConfig = workspace.getConfiguration('python-envs', undefined); + const envManager = globalConfig.get('defaultEnvManager', DEFAULT_ENV_MANAGER_ID); + const pkgManager = globalConfig.get('defaultPackageManager', DEFAULT_PACKAGE_MANAGER_ID); + + edits.forEach((e) => { + const w = workspace.getWorkspaceFolder(e.project.uri); + if (w) { + workspaces.set(w, [...(workspaces.get(w) || []), e]); } else { - await config.update('defaultPackageManager', managerId, ConfigurationTarget.Workspace); + noWorkspace.push(e); } - } else { - const config = workspace.getConfiguration('python-envs', undefined); - await config.update('defaultPackageManager', managerId, ConfigurationTarget.Global); - } -} + }); + + noWorkspace.forEach((e) => { + traceError(`Unable to find workspace for ${e.project.uri.fsPath}`); + }); -export async function addPythonProjectSetting( - pw: PythonProject, - envManager: string, - pkgManager: string, -): Promise { - const w = workspace.getWorkspaceFolder(pw.uri); - if (w) { + const isMultiroot = (getWorkspaceFolders() ?? []).length > 1; + + const promises: Thenable[] = []; + workspaces.forEach((es, w) => { const config = workspace.getConfiguration('python-envs', w.uri); const overrides = config.get('pythonProjects', []); - const pwPath = path.normalize(pw.uri.fsPath); - const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); - if (index >= 0) { - overrides[index].envManager = envManager; - overrides[index].packageManager = pkgManager; + es.forEach((e) => { + if (isMultiroot) { + } + const pwPath = path.normalize(e.project.uri.fsPath); + const index = overrides.findIndex((s) => { + if (s.workspace) { + // If the workspace is set, check workspace and path in existing overrides + return s.workspace === w.name && path.resolve(w.uri.fsPath, s.path) === pwPath; + } + return path.resolve(w.uri.fsPath, s.path) === pwPath; + }); + if (index >= 0) { + overrides[index].envManager = e.envManager ?? envManager; + overrides[index].packageManager = e.packageManager ?? pkgManager; + } else { + overrides.push({ + path: path.relative(w.uri.fsPath, pwPath).replace(/\\/g, '/'), + envManager, + packageManager: pkgManager, + workspace: isMultiroot ? w.name : undefined, + }); + } + }); + promises.push(config.update('pythonProjects', overrides, ConfigurationTarget.Workspace)); + }); + await Promise.all(promises); +} + +export async function removePythonProjectSetting(edits: EditProjectSettings[]): Promise { + const noWorkspace: EditProjectSettings[] = []; + const workspaces = new Map(); + edits.forEach((e) => { + const w = workspace.getWorkspaceFolder(e.project.uri); + if (w) { + workspaces.set(w, [...(workspaces.get(w) || []), e]); } else { - overrides.push({ path: path.relative(w.uri.fsPath, pwPath), envManager, packageManager: pkgManager }); + noWorkspace.push(e); } - await config.update('pythonProjects', overrides, ConfigurationTarget.Workspace); - } else { - traceError(`Unable to find workspace for ${pw.uri.fsPath}`); - } -} + }); -export async function removePythonProjectSetting(pw: PythonProject): Promise { - const w = workspace.getWorkspaceFolder(pw.uri); - if (w) { + noWorkspace.forEach((e) => { + traceError(`Unable to find workspace for ${e.project.uri.fsPath}`); + }); + + const promises: Thenable[] = []; + workspaces.forEach((es, w) => { const config = workspace.getConfiguration('python-envs', w.uri); const overrides = config.get('pythonProjects', []); - const pwPath = path.normalize(pw.uri.fsPath); - const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); - if (index >= 0) { - overrides.splice(index, 1); - await config.update('pythonProjects', overrides, ConfigurationTarget.Workspace); + es.forEach((e) => { + const pwPath = path.normalize(e.project.uri.fsPath); + const index = overrides.findIndex((s) => path.resolve(w.uri.fsPath, s.path) === pwPath); + if (index >= 0) { + overrides.splice(index, 1); + } + }); + if (overrides.length === 0) { + promises.push(config.update('pythonProjects', undefined, ConfigurationTarget.Workspace)); + } else { + promises.push(config.update('pythonProjects', overrides, ConfigurationTarget.Workspace)); } - } else { - traceError(`Unable to find workspace for ${pw.uri.fsPath}`); + }); + await Promise.all(promises); +} + +/** + * Gets user-configured setting for window-scoped settings. + * Priority order: globalRemoteValue > globalLocalValue > globalValue + * @param section - The configuration section (e.g., 'python-envs') + * @param key - The configuration key (e.g., 'terminal.autoActivationType') + * @returns The user-configured value or undefined if not set by user + */ +export function getSettingWindowScope(section: string, key: string): T | undefined { + const config = getConfiguration(section); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; } + + const inspectRecord = inspect as Record; + if ('globalRemoteValue' in inspect && inspectRecord.globalRemoteValue !== undefined) { + return inspectRecord.globalRemoteValue as T; + } + if ('globalLocalValue' in inspect && inspectRecord.globalLocalValue !== undefined) { + return inspectRecord.globalLocalValue as T; + } + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; +} + +/** + * Gets user-configured setting for workspace-scoped settings. + * Priority order: workspaceFolderValue > workspaceValue > globalValue + * @param section - The configuration section (e.g., 'python') + * @param key - The configuration key (e.g., 'pipenvPath') + * @param scope - Optional URI scope for workspace folder-specific settings + * @returns The user-configured value or undefined if not set by user + */ +export function getSettingWorkspaceScope(section: string, key: string, scope?: Uri): T | undefined { + const config = getConfiguration(section, scope); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; + } + + if (inspect.workspaceFolderValue !== undefined) { + return inspect.workspaceFolderValue; + } + if (inspect.workspaceValue !== undefined) { + return inspect.workspaceValue; + } + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; +} + +/** + * Gets user-configured setting for user-scoped settings. + * Only checks globalValue (ignores defaultValue). + * @param section - The configuration section (e.g., 'python') + * @param key - The configuration key (e.g., 'pipenvPath') + * @returns The user-configured value or undefined if not set by user + */ +export function getSettingUserScope(section: string, key: string): T | undefined { + const config = getConfiguration(section); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; + } + + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; } diff --git a/src/features/statusBarPython.ts b/src/features/statusBarPython.ts deleted file mode 100644 index 2e5de93..0000000 --- a/src/features/statusBarPython.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Disposable, StatusBarAlignment, StatusBarItem, ThemeColor, Uri } from 'vscode'; -import { createStatusBarItem } from '../common/window.apis'; -import { Interpreter } from '../common/localize'; -import { PythonProjectManager } from '../internal.api'; -import { PythonEnvironment } from '../api'; - -const STATUS_BAR_ITEM_PRIORITY = 100.09999; - -export interface PythonStatusBar extends Disposable { - update(uri: Uri | undefined, env?: PythonEnvironment): void; - show(uri: Uri): void; - hide(): void; -} - -export class PythonStatusBarImpl implements PythonStatusBar { - private _global: PythonEnvironment | undefined; - private _statusBarItem: StatusBarItem; - private _disposables: Disposable[] = []; - private _uriToEnv: Map = new Map(); - constructor(private readonly projectManager: PythonProjectManager) { - this._statusBarItem = createStatusBarItem( - 'python-envs.statusBarItem.selectedInterpreter', - StatusBarAlignment.Right, - STATUS_BAR_ITEM_PRIORITY, - ); - this._statusBarItem.command = 'python-envs.set'; - this._disposables.push(this._statusBarItem); - } - - public update(uri: Uri | undefined, env?: PythonEnvironment): void { - const project = uri ? this.projectManager.get(uri)?.uri : undefined; - if (!project) { - this._global = env; - } else { - if (env) { - this._uriToEnv.set(project.toString(), env); - } else { - this._uriToEnv.delete(project.toString()); - } - } - } - public show(uri: Uri | undefined) { - const project = uri ? this.projectManager.get(uri)?.uri : undefined; - const environment = project ? this._uriToEnv.get(project.toString()) : this._global; - if (environment) { - this._statusBarItem.text = environment.shortDisplayName ?? environment.displayName; - this._statusBarItem.tooltip = environment.environmentPath.fsPath; - this._statusBarItem.backgroundColor = undefined; - this._statusBarItem.show(); - return; - } else if (project) { - // Show alert only if it is a project file - this._statusBarItem.tooltip = ''; - this._statusBarItem.backgroundColor = new ThemeColor('statusBarItem.warningBackground'); - this._statusBarItem.text = `$(alert) ${Interpreter.statusBarSelect}`; - this._statusBarItem.show(); - return; - } - - this._statusBarItem.hide(); - } - - public hide() { - this._statusBarItem.hide(); - } - - dispose() { - this._disposables.forEach((d) => d.dispose()); - } -} diff --git a/src/features/terminal/activateMenuButton.ts b/src/features/terminal/activateMenuButton.ts new file mode 100644 index 0000000..b271a05 --- /dev/null +++ b/src/features/terminal/activateMenuButton.ts @@ -0,0 +1,18 @@ +import { Terminal } from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { isActivatableEnvironment } from '../common/activation'; +import { executeCommand } from '../../common/command.api'; +import { isTaskTerminal } from './utils'; + +export async function setActivateMenuButtonContext( + terminal: Terminal, + env: PythonEnvironment, + activated?: boolean, +): Promise { + const activatable = !isTaskTerminal(terminal) && isActivatableEnvironment(env); + await executeCommand('setContext', 'pythonTerminalActivation', activatable); + + if (activated !== undefined) { + await executeCommand('setContext', 'pythonTerminalActivated', activated); + } +} diff --git a/src/features/terminal/runInTerminal.ts b/src/features/terminal/runInTerminal.ts new file mode 100644 index 0000000..24ac7eb --- /dev/null +++ b/src/features/terminal/runInTerminal.ts @@ -0,0 +1,53 @@ +import { Terminal, TerminalShellExecution } from 'vscode'; +import { PythonEnvironment, PythonTerminalExecutionOptions } from '../../api'; +import { createDeferred } from '../../common/utils/deferred'; +import { onDidEndTerminalShellExecution } from '../../common/window.apis'; +import { ShellConstants } from '../common/shellConstants'; +import { identifyTerminalShell } from '../common/shellDetector'; +import { quoteArgs } from '../execution/execUtils'; + +export async function runInTerminal( + environment: PythonEnvironment, + terminal: Terminal, + options: PythonTerminalExecutionOptions, +): Promise { + if (options.show) { + terminal.show(); + } + + let executable = environment.execInfo?.activatedRun?.executable ?? environment.execInfo?.run.executable ?? 'python'; + const args = environment.execInfo?.activatedRun?.args ?? environment.execInfo?.run.args ?? []; + const allArgs = [...args, ...(options.args ?? [])]; + const shellType = identifyTerminalShell(terminal); + if (terminal.shellIntegration) { + let execution: TerminalShellExecution | undefined; + const deferred = createDeferred(); + const disposable = onDidEndTerminalShellExecution((e) => { + if (e.execution === execution) { + disposable.dispose(); + deferred.resolve(); + } + }); + + const shouldSurroundWithQuotes = + executable.includes(' ') && !executable.startsWith('"') && !executable.endsWith('"'); + // Handle case where executable contains white-spaces. + if (shouldSurroundWithQuotes) { + executable = `"${executable}"`; + } + + if (shellType === ShellConstants.PWSH && !executable.startsWith('&')) { + // PowerShell requires commands to be prefixed with '&' to run them. + executable = `& ${executable}`; + } + execution = terminal.shellIntegration.executeCommand(executable, allArgs); + await deferred.promise; + } else { + let text = quoteArgs([executable, ...allArgs]).join(' '); + if (shellType === ShellConstants.PWSH && !text.startsWith('&')) { + // PowerShell requires commands to be prefixed with '&' to run them. + text = `& ${text}`; + } + terminal.sendText(`${text}\n`); + } +} diff --git a/src/features/terminal/shellStartupActivationVariablesManager.ts b/src/features/terminal/shellStartupActivationVariablesManager.ts new file mode 100644 index 0000000..3ea63a0 --- /dev/null +++ b/src/features/terminal/shellStartupActivationVariablesManager.ts @@ -0,0 +1,111 @@ +import { ConfigurationChangeEvent, Disposable, GlobalEnvironmentVariableCollection } from 'vscode'; +import { DidChangeEnvironmentEventArgs, PythonProjectEnvironmentApi } from '../../api'; +import { ActivationStrings } from '../../common/localize'; +import { getWorkspaceFolder, getWorkspaceFolders, onDidChangeConfiguration } from '../../common/workspace.apis'; +import { ShellEnvsProvider } from './shells/startupProvider'; +import { ACT_TYPE_SHELL, getAutoActivationType } from './utils'; + +export interface ShellStartupActivationVariablesManager extends Disposable { + initialize(): Promise; +} + +export class ShellStartupActivationVariablesManagerImpl implements ShellStartupActivationVariablesManager { + private readonly disposables: Disposable[] = []; + constructor( + private readonly envCollection: GlobalEnvironmentVariableCollection, + private readonly shellEnvsProviders: ShellEnvsProvider[], + private readonly api: PythonProjectEnvironmentApi, + ) { + this.envCollection.description = ActivationStrings.envCollectionDescription; + this.disposables.push( + onDidChangeConfiguration(async (e: ConfigurationChangeEvent) => { + await this.handleConfigurationChange(e); + }), + this.api.onDidChangeEnvironment(async (e: DidChangeEnvironmentEventArgs) => { + await this.handleEnvironmentChange(e); + }), + ); + } + + private async handleConfigurationChange(e: ConfigurationChangeEvent) { + if (e.affectsConfiguration('python-envs.terminal.autoActivationType')) { + const autoActType = getAutoActivationType(); + if (autoActType === ACT_TYPE_SHELL) { + await this.initializeInternal(); + } else { + const workspaces = getWorkspaceFolders() ?? []; + if (workspaces.length > 0) { + workspaces.forEach((workspace) => { + const collection = this.envCollection.getScoped({ workspaceFolder: workspace }); + this.shellEnvsProviders.forEach((provider) => provider.removeEnvVariables(collection)); + }); + } else { + this.shellEnvsProviders.forEach((provider) => provider.removeEnvVariables(this.envCollection)); + } + } + } + } + + private async handleEnvironmentChange(e: DidChangeEnvironmentEventArgs) { + const autoActType = getAutoActivationType(); + if (autoActType === ACT_TYPE_SHELL && e.uri) { + const wf = getWorkspaceFolder(e.uri); + if (wf) { + const envVars = this.envCollection.getScoped({ workspaceFolder: wf }); + if (envVars) { + this.shellEnvsProviders.forEach((provider) => { + if (e.new) { + provider.updateEnvVariables(envVars, e.new); + } else { + provider.removeEnvVariables(envVars); + } + }); + } + } + } + } + + private async initializeInternal(): Promise { + const workspaces = getWorkspaceFolders() ?? []; + + if (workspaces.length > 0) { + const promises: Promise[] = []; + workspaces.forEach((workspace) => { + const collection = this.envCollection.getScoped({ workspaceFolder: workspace }); + promises.push( + ...this.shellEnvsProviders.map(async (provider) => { + const env = await this.api.getEnvironment(workspace.uri); + if (env) { + provider.updateEnvVariables(collection, env); + } else { + provider.removeEnvVariables(collection); + } + }), + ); + }); + await Promise.all(promises); + } else { + const env = await this.api.getEnvironment(undefined); + await Promise.all( + this.shellEnvsProviders.map(async (provider) => { + if (env) { + provider.updateEnvVariables(this.envCollection, env); + } else { + provider.removeEnvVariables(this.envCollection); + } + }), + ); + } + } + + public async initialize(): Promise { + const autoActType = getAutoActivationType(); + if (autoActType === ACT_TYPE_SHELL) { + await this.initializeInternal(); + } + } + + dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } +} diff --git a/src/features/terminal/shellStartupSetupHandlers.ts b/src/features/terminal/shellStartupSetupHandlers.ts new file mode 100644 index 0000000..b9da1e8 --- /dev/null +++ b/src/features/terminal/shellStartupSetupHandlers.ts @@ -0,0 +1,70 @@ +import { l10n, ProgressLocation } from 'vscode'; +import { executeCommand } from '../../common/command.api'; +import { ActivationStrings, Common } from '../../common/localize'; +import { traceInfo, traceVerbose } from '../../common/logging'; +import { showErrorMessage, showInformationMessage, withProgress } from '../../common/window.apis'; +import { ShellScriptEditState, ShellStartupScriptProvider } from './shells/startupProvider'; +import { ACT_TYPE_COMMAND, ACT_TYPE_SHELL, getAutoActivationType, setAutoActivationType } from './utils'; + +export async function handleSettingUpShellProfile( + providers: ShellStartupScriptProvider[], + callback: (provider: ShellStartupScriptProvider, result: boolean) => void, +): Promise { + const shells = providers.map((p) => p.shellType).join(', '); + // Only show prompt when shell integration is not available, or disabled. + const response = await showInformationMessage( + l10n.t( + 'To enable "{0}" activation, your shell profile(s) may need to be updated to include the necessary startup scripts. Would you like to proceed with these changes?', + ACT_TYPE_SHELL, + ), + { modal: true, detail: l10n.t('Shells: {0}', shells) }, + Common.yes, + ); + + if (response === Common.yes) { + traceVerbose(`User chose to set up shell profiles for ${shells} shells`); + const states = await withProgress( + { + location: ProgressLocation.Notification, + title: l10n.t('Setting up shell profiles for {0}', shells), + }, + async () => { + return (await Promise.all(providers.map((provider) => provider.setupScripts()))).filter( + (state) => state !== ShellScriptEditState.NotInstalled, + ); + }, + ); + if (states.every((state) => state === ShellScriptEditState.Edited)) { + setImmediate(async () => { + await showInformationMessage( + l10n.t( + 'Shell profiles have been set up successfully. Extension will use shell startup activation next time a new terminal is created.', + ), + ); + }); + providers.forEach((provider) => callback(provider, true)); + } else { + setImmediate(async () => { + const button = await showErrorMessage( + l10n.t('Failed to set up shell profiles. Please check the output panel for more details.'), + Common.viewLogs, + ); + if (button === Common.viewLogs) { + await executeCommand('python-envs.viewLogs'); + } + }); + providers.forEach((provider) => callback(provider, false)); + } + } +} + +export async function cleanupStartupScripts(allProviders: ShellStartupScriptProvider[]): Promise { + await Promise.all(allProviders.map((provider) => provider.teardownScripts())); + if (getAutoActivationType() === ACT_TYPE_SHELL) { + setAutoActivationType(ACT_TYPE_COMMAND); + traceInfo( + 'Setting `python-envs.terminal.autoActivationType` to `command`, after removing shell startup scripts.', + ); + } + setImmediate(async () => await showInformationMessage(ActivationStrings.revertedShellStartupScripts)); +} diff --git a/src/features/terminal/shells/bash/bashConstants.ts b/src/features/terminal/shells/bash/bashConstants.ts new file mode 100644 index 0000000..72c838e --- /dev/null +++ b/src/features/terminal/shells/bash/bashConstants.ts @@ -0,0 +1,5 @@ +export const BASH_ENV_KEY = 'VSCODE_PYTHON_BASH_ACTIVATE'; +export const ZSH_ENV_KEY = 'VSCODE_PYTHON_ZSH_ACTIVATE'; +export const BASH_OLD_ENV_KEY = 'VSCODE_BASH_ACTIVATE'; +export const ZSH_OLD_ENV_KEY = 'VSCODE_ZSH_ACTIVATE'; +export const BASH_SCRIPT_VERSION = '0.1.1'; diff --git a/src/features/terminal/shells/bash/bashEnvs.ts b/src/features/terminal/shells/bash/bashEnvs.ts new file mode 100644 index 0000000..08219b4 --- /dev/null +++ b/src/features/terminal/shells/bash/bashEnvs.ts @@ -0,0 +1,96 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../../api'; +import { traceError } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; +import { ShellEnvsProvider } from '../startupProvider'; +import { BASH_ENV_KEY, ZSH_ENV_KEY } from './bashConstants'; + +export class BashEnvsProvider implements ShellEnvsProvider { + constructor(public readonly shellType: 'bash' | 'gitbash') {} + + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const bashActivation = getShellActivationCommand(this.shellType, env); + if (bashActivation) { + const command = getShellCommandAsString(this.shellType, bashActivation); + const v = collection.get(BASH_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(BASH_ENV_KEY, command); + } else { + collection.delete(BASH_ENV_KEY); + } + } catch (err) { + traceError(`Failed to update env variables for ${this.shellType}`, err); + collection.delete(BASH_ENV_KEY); + } + } + + removeEnvVariables(envCollection: EnvironmentVariableCollection): void { + envCollection.delete(BASH_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[BASH_ENV_KEY, undefined]]); + } + + try { + const bashActivation = getShellActivationCommand(this.shellType, env); + if (bashActivation) { + const command = getShellCommandAsString(this.shellType, bashActivation); + return new Map([[BASH_ENV_KEY, command]]); + } + return undefined; + } catch (err) { + traceError(`Failed to get env variables for ${this.shellType}`, err); + return undefined; + } + } +} + +export class ZshEnvsProvider implements ShellEnvsProvider { + public readonly shellType: string = ShellConstants.ZSH; + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const zshActivation = getShellActivationCommand(this.shellType, env); + if (zshActivation) { + const command = getShellCommandAsString(this.shellType, zshActivation); + const v = collection.get(ZSH_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(ZSH_ENV_KEY, command); + } else { + collection.delete(ZSH_ENV_KEY); + } + } catch (err) { + traceError('Failed to update env variables for zsh', err); + collection.delete(ZSH_ENV_KEY); + } + } + + removeEnvVariables(collection: EnvironmentVariableCollection): void { + collection.delete(ZSH_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[ZSH_ENV_KEY, undefined]]); + } + + try { + const zshActivation = getShellActivationCommand(this.shellType, env); + if (zshActivation) { + const command = getShellCommandAsString(this.shellType, zshActivation); + return new Map([[ZSH_ENV_KEY, command]]); + } + return undefined; + } catch (err) { + traceError('Failed to get env variables for zsh', err); + return undefined; + } + } +} diff --git a/src/features/terminal/shells/bash/bashStartup.ts b/src/features/terminal/shells/bash/bashStartup.ts new file mode 100644 index 0000000..db9be72 --- /dev/null +++ b/src/features/terminal/shells/bash/bashStartup.ts @@ -0,0 +1,310 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import which from 'which'; +import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; +import { getShellIntegrationEnabledCache, isWsl, shellIntegrationForActiveTerminal } from '../common/shellUtils'; +import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; +import { BASH_ENV_KEY, BASH_OLD_ENV_KEY, BASH_SCRIPT_VERSION, ZSH_ENV_KEY, ZSH_OLD_ENV_KEY } from './bashConstants'; + +async function isBashLikeInstalled(): Promise { + const result = await Promise.all([which('bash', { nothrow: true }), which('sh', { nothrow: true })]); + return result.some((r) => r !== null); +} + +async function isZshInstalled(): Promise { + const result = await which('zsh', { nothrow: true }); + return result !== null; +} + +async function isGitBashInstalled(): Promise { + const gitPath = await which('git', { nothrow: true }); + if (gitPath) { + const gitBashPath = path.join(path.dirname(path.dirname(gitPath)), 'bin', 'bash.exe'); + return await fs.pathExists(gitBashPath); + } + return false; +} + +async function getBashProfiles(): Promise { + const homeDir = os.homedir(); + const profile: string = path.join(homeDir, '.bashrc'); + + return profile; +} + +async function getZshProfiles(): Promise { + const zdotdir = process.env.ZDOTDIR; + const baseDir = zdotdir || os.homedir(); + const profile: string = path.join(baseDir, '.zshrc'); + + return profile; +} + +const regionStart = '# >>> vscode python'; +const regionEnd = '# <<< vscode python'; + +function getActivationContent(key: string): string { + const lineSep = '\n'; + return [ + `# version: ${BASH_SCRIPT_VERSION}`, + `if [ -z "$VSCODE_PYTHON_AUTOACTIVATE_GUARD" ]; then`, + ` export VSCODE_PYTHON_AUTOACTIVATE_GUARD=1`, + ` if [ -n "$${key}" ] && [ "$TERM_PROGRAM" = "vscode" ]; then`, + ` eval "$${key}" || true`, + ` fi`, + `fi`, + ].join(lineSep); +} + +async function isStartupSetup(profile: string, key: string): Promise { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + return ShellSetupState.Setup; + } + } + return ShellSetupState.NotSetup; +} +async function setupStartup(profile: string, key: string, name: string): Promise { + const shellIntegrationEnabled = await getShellIntegrationEnabledCache(); + if ((shellIntegrationEnabled || (await shellIntegrationForActiveTerminal(name, profile))) && !isWsl()) { + removeStartup(profile, key); + return true; + } + const activationContent = getActivationContent(key); + try { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + traceInfo(`SHELL: ${name} profile already contains activation code at: ${profile}`); + } else { + await fs.writeFile(profile, insertStartupCode(content, regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Updated existing ${name} profile at: ${profile}\n${activationContent}`); + } + } else { + await fs.mkdirp(path.dirname(profile)); + await fs.writeFile(profile, insertStartupCode('', regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Created new ${name} profile at: ${profile}\n${activationContent}`); + } + + return true; + } catch (err) { + traceError(`SHELL: Failed to setup startup for profile at: ${profile}`, err); + return false; + } +} + +async function removeStartup(profile: string, key: string): Promise { + if (!(await fs.pathExists(profile))) { + return true; + } + + try { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + await fs.writeFile(profile, removeStartupCode(content, regionStart, regionEnd)); + traceInfo(`SHELL: Removed activation from profile at: ${profile}, for key: ${key}`); + } else { + traceVerbose(`Profile at ${profile} does not contain activation code, for key: ${key}`); + } + return true; + } catch (err) { + traceVerbose(`Failed to remove ${profile} startup, for key: ${key}`, err); + return false; + } +} + +export class BashStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'bash'; + public readonly shellType: string = ShellConstants.BASH; + + private async checkShellInstalled(): Promise { + const found = await isBashLikeInstalled(); + if (!found) { + traceInfo( + '`bash` or `sh` was not found on the system', + 'If it is installed make sure it is available on `PATH`', + ); + } + return found; + } + + async isSetup(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellSetupState.NotInstalled; + } + + try { + const bashProfile = await getBashProfiles(); + return await isStartupSetup(bashProfile, BASH_ENV_KEY); + } catch (err) { + traceError('Failed to check bash startup scripts', err); + return ShellSetupState.NotSetup; + } + } + + async setupScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + + try { + const bashProfiles = await getBashProfiles(); + const result = await setupStartup(bashProfiles, BASH_ENV_KEY, this.name); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup bash startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + + async teardownScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + + try { + const bashProfile = await getBashProfiles(); + // Remove old environment variable if it exists + await removeStartup(bashProfile, BASH_OLD_ENV_KEY); + const result = await removeStartup(bashProfile, BASH_ENV_KEY); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to teardown bash startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + + clearCache(): Promise { + return Promise.resolve(); + } +} + +export class ZshStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'zsh'; + public readonly shellType: string = ShellConstants.ZSH; + + private async checkShellInstalled(): Promise { + const found = await isZshInstalled(); + if (!found) { + traceInfo('`zsh` was not found on the system', 'If it is installed make sure it is available on `PATH`'); + } + return found; + } + + async isSetup(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellSetupState.NotInstalled; + } + + try { + const zshProfiles = await getZshProfiles(); + return await isStartupSetup(zshProfiles, ZSH_ENV_KEY); + } catch (err) { + traceError('Failed to check zsh startup scripts', err); + return ShellSetupState.NotSetup; + } + } + + async setupScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + try { + const zshProfiles = await getZshProfiles(); + const result = await setupStartup(zshProfiles, ZSH_ENV_KEY, this.name); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup zsh startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + + async teardownScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + try { + const zshProfiles = await getZshProfiles(); + await removeStartup(zshProfiles, ZSH_OLD_ENV_KEY); + const result = await removeStartup(zshProfiles, ZSH_ENV_KEY); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to teardown zsh startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + clearCache(): Promise { + return Promise.resolve(); + } +} + +export class GitBashStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'Git bash'; + public readonly shellType: string = ShellConstants.GITBASH; + + private async checkShellInstalled(): Promise { + const found = await isGitBashInstalled(); + if (!found) { + traceInfo('Git Bash was not found on the system', 'If it is installed make sure it is available on `PATH`'); + } + return found; + } + + async isSetup(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellSetupState.NotInstalled; + } + try { + const bashProfiles = await getBashProfiles(); + return await isStartupSetup(bashProfiles, BASH_ENV_KEY); + } catch (err) { + traceError('Failed to check git bash startup scripts', err); + return ShellSetupState.NotSetup; + } + } + async setupScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + + try { + const bashProfiles = await getBashProfiles(); + const result = await setupStartup(bashProfiles, BASH_ENV_KEY, this.name); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup git bash startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + async teardownScripts(): Promise { + const found = await this.checkShellInstalled(); + if (!found) { + return ShellScriptEditState.NotInstalled; + } + + try { + const bashProfiles = await getBashProfiles(); + await removeStartup(bashProfiles, BASH_OLD_ENV_KEY); + const result = await removeStartup(bashProfiles, BASH_ENV_KEY); + return result ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to teardown git bash startup scripts', err); + return ShellScriptEditState.NotEdited; + } + } + clearCache(): Promise { + return Promise.resolve(); + } +} diff --git a/src/features/terminal/shells/cmd/cmdConstants.ts b/src/features/terminal/shells/cmd/cmdConstants.ts new file mode 100644 index 0000000..1f244c9 --- /dev/null +++ b/src/features/terminal/shells/cmd/cmdConstants.ts @@ -0,0 +1,2 @@ +export const CMD_ENV_KEY = 'VSCODE_PYTHON_CMD_ACTIVATE'; +export const CMD_SCRIPT_VERSION = '0.1.0'; diff --git a/src/features/terminal/shells/cmd/cmdEnvs.ts b/src/features/terminal/shells/cmd/cmdEnvs.ts new file mode 100644 index 0000000..fbeef3f --- /dev/null +++ b/src/features/terminal/shells/cmd/cmdEnvs.ts @@ -0,0 +1,50 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../../api'; +import { traceError } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; +import { ShellEnvsProvider } from '../startupProvider'; +import { CMD_ENV_KEY } from './cmdConstants'; + +export class CmdEnvsProvider implements ShellEnvsProvider { + readonly shellType: string = ShellConstants.CMD; + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const cmdActivation = getShellActivationCommand(this.shellType, env); + if (cmdActivation) { + const command = getShellCommandAsString(this.shellType, cmdActivation); + const v = collection.get(CMD_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(CMD_ENV_KEY, command); + } else { + collection.delete(CMD_ENV_KEY); + } + } catch (err) { + traceError('Failed to update CMD environment variables', err); + collection.delete(CMD_ENV_KEY); + } + } + + removeEnvVariables(envCollection: EnvironmentVariableCollection): void { + envCollection.delete(CMD_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[CMD_ENV_KEY, undefined]]); + } + + try { + const cmdActivation = getShellActivationCommand(this.shellType, env); + if (cmdActivation) { + return new Map([[CMD_ENV_KEY, getShellCommandAsString(this.shellType, cmdActivation)]]); + } + return undefined; + } catch (err) { + traceError('Failed to get CMD environment variables', err); + return undefined; + } + } +} diff --git a/src/features/terminal/shells/cmd/cmdStartup.ts b/src/features/terminal/shells/cmd/cmdStartup.ts new file mode 100644 index 0000000..3b542ff --- /dev/null +++ b/src/features/terminal/shells/cmd/cmdStartup.ts @@ -0,0 +1,319 @@ +import * as cp from 'child_process'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { promisify } from 'util'; +import which from 'which'; +import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; +import { isWindows } from '../../../../common/utils/platformUtils'; +import { ShellConstants } from '../../../common/shellConstants'; +import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; +import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; +import { CMD_ENV_KEY, CMD_SCRIPT_VERSION } from './cmdConstants'; +import { StopWatch } from '../../../../common/stopWatch'; + +function execCommand(command: string) { + const timer = new StopWatch(); + return promisify(cp.exec)(command, { windowsHide: true }).finally(() => + traceInfo(`Executed command: ${command} in ${timer.elapsedTime}`), + ); +} + +async function isCmdInstalled(): Promise { + if (!isWindows()) { + return false; + } + + if (process.env.ComSpec && (await fs.exists(process.env.ComSpec))) { + return true; + } + + try { + // Try to find cmd.exe on the system + await which('cmd.exe', { nothrow: true }); + return true; + } catch { + // This should normally not happen on Windows + return false; + } +} + +interface CmdFilePaths { + startupFile: string; + regStartupFile: string; + mainBatchFile: string; + regMainBatchFile: string; + mainName: string; + startupName: string; +} + +async function getCmdFilePaths(): Promise { + const homeDir = process.env.USERPROFILE ?? os.homedir(); + const cmdrcDir = path.join(homeDir, '.cmdrc'); + await fs.ensureDir(cmdrcDir); + + return { + mainBatchFile: path.join(cmdrcDir, 'cmd_startup.bat'), + regMainBatchFile: path.join('%USERPROFILE%', '.cmdrc', 'cmd_startup.bat'), + mainName: 'cmd_startup.bat', + startupFile: path.join(cmdrcDir, 'vscode-python.bat'), + regStartupFile: path.join('%USERPROFILE%', '.cmdrc', 'vscode-python.bat'), + startupName: 'vscode-python.bat', + }; +} + +const regionStart = ':: >>> vscode python'; +const regionEnd = ':: <<< vscode python'; + +function getActivationContent(key: string): string { + const lineSep = '\r\n'; + return [`:: version: ${CMD_SCRIPT_VERSION}`, `if defined ${key} (`, ` call %${key}%`, ')'].join(lineSep); +} + +function getHeader(): string { + const lineSep = '\r\n'; + const content = []; + content.push('@echo off'); + content.push(':: startup used in HKCU\\Software\\Microsoft\\Command Processor key AutoRun'); + content.push(''); + return content.join(lineSep); +} + +function getMainBatchFileContent(startupFile: string): string { + const lineSep = '\r\n'; + const content = []; + + content.push('if "%TERM_PROGRAM%"=="vscode" ('); + content.push(' if not defined VSCODE_PYTHON_AUTOACTIVATE_GUARD ('); + content.push(' set "VSCODE_PYTHON_AUTOACTIVATE_GUARD=1"'); + content.push(` if exist "${startupFile}" call "${startupFile}"`); + content.push(' )'); + content.push(')'); + + return content.join(lineSep); +} + +async function checkRegistryAutoRun(mainBatchFile: string, regMainBatchFile: string): Promise { + if (!isWindows()) { + return false; + } + + try { + // Check if AutoRun is set in the registry to call our batch file + const { stdout } = await execCommand('reg query "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun'); + + // Check if the output contains our batch file path + return stdout.includes(regMainBatchFile) || stdout.includes(mainBatchFile); + } catch { + // If the command fails, the registry key might not exist + return false; + } +} + +async function getExistingAutoRun(): Promise { + if (!isWindows()) { + return undefined; + } + + try { + const { stdout } = await execCommand('reg query "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun'); + + const match = stdout.match(/AutoRun\s+REG_SZ\s+(.*)/); + if (match && match[1]) { + const content = match[1].trim(); + return content; + } + } catch { + // Key doesn't exist yet + } + + return undefined; +} + +async function setupRegistryAutoRun(mainBatchFile: string): Promise { + if (!isWindows()) { + return false; + } + + try { + // Set the registry key to call our main batch file + await execCommand( + `reg add "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /t REG_SZ /d "if exist \\"${mainBatchFile}\\" call \\"${mainBatchFile}\\"" /f`, + ); + + traceInfo( + `Set CMD AutoRun registry key [HKCU\\Software\\Microsoft\\Command Processor] to call: ${mainBatchFile}`, + ); + return true; + } catch (err) { + traceError('Failed to set CMD AutoRun registry key [HKCU\\Software\\Microsoft\\Command Processor]', err); + return false; + } +} + +async function isCmdStartupSetup(cmdFiles: CmdFilePaths, key: string): Promise { + const fileExists = await fs.pathExists(cmdFiles.startupFile); + let fileHasContent = false; + if (fileExists) { + const content = await fs.readFile(cmdFiles.startupFile, 'utf8'); + fileHasContent = hasStartupCode(content, regionStart, regionEnd, [key]); + } + + if (!fileHasContent) { + return ShellSetupState.NotSetup; + } + + const mainFileExists = await fs.pathExists(cmdFiles.mainBatchFile); + let mainFileHasContent = false; + if (mainFileExists) { + const mainFileContent = await fs.readFile(cmdFiles.mainBatchFile, 'utf8'); + mainFileHasContent = hasStartupCode(mainFileContent, regionStart, regionEnd, [cmdFiles.startupName]); + } + + if (!mainFileHasContent) { + return ShellSetupState.NotSetup; + } + + const registrySetup = await checkRegistryAutoRun(cmdFiles.regMainBatchFile, cmdFiles.mainBatchFile); + return registrySetup ? ShellSetupState.Setup : ShellSetupState.NotSetup; +} + +async function setupCmdStartup(cmdFiles: CmdFilePaths, key: string): Promise { + try { + const activationContent = getActivationContent(key); + + // Step 1: Create or update the activation file + if (await fs.pathExists(cmdFiles.startupFile)) { + const content = await fs.readFile(cmdFiles.startupFile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + traceInfo(`SHELL: CMD activation file at ${cmdFiles.startupFile} already contains activation code`); + } else { + await fs.writeFile( + cmdFiles.startupFile, + insertStartupCode(content, regionStart, regionEnd, activationContent), + ); + traceInfo( + `SHELL: Updated existing CMD activation file at: ${cmdFiles.startupFile}\r\n${activationContent}`, + ); + } + } else { + await fs.writeFile( + cmdFiles.startupFile, + insertStartupCode(getHeader(), regionStart, regionEnd, activationContent), + ); + traceInfo(`SHELL: Created new CMD activation file at: ${cmdFiles.startupFile}\r\n${activationContent}`); + } + + // Step 2: Get existing AutoRun content + const existingAutoRun = await getExistingAutoRun(); + + // Step 3: Create or update the main batch file + if (await fs.pathExists(cmdFiles.mainBatchFile)) { + const content = await fs.readFile(cmdFiles.mainBatchFile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [cmdFiles.startupName])) { + traceInfo(`SHELL: CMD main batch file at ${cmdFiles.mainBatchFile} already contains our startup file`); + } else { + const mainBatchContent = getMainBatchFileContent(cmdFiles.regStartupFile); + await fs.writeFile( + cmdFiles.mainBatchFile, + insertStartupCode(content, regionStart, regionEnd, mainBatchContent), + ); + traceInfo( + `SHELL: Updated existing main batch file at: ${cmdFiles.mainBatchFile}\r\n${mainBatchContent}`, + ); + } + } + + // Step 4: Setup registry AutoRun to call our main batch file + if (existingAutoRun?.includes(cmdFiles.regMainBatchFile) || existingAutoRun?.includes(cmdFiles.mainBatchFile)) { + traceInfo(`SHELL: CMD AutoRun registry key already contains our main batch file`); + } else { + const registrySetup = await setupRegistryAutoRun(cmdFiles.mainBatchFile); + return registrySetup; + } + return true; + } catch (err) { + traceVerbose(`Failed to setup CMD startup`, err); + return false; + } +} + +async function removeCmdStartup(startupFile: string, key: string): Promise { + // Note: We deliberately DO NOT remove the main batch file or registry AutoRun setting + // This allows other components to continue using the AutoRun functionality + if (await fs.pathExists(startupFile)) { + try { + const content = await fs.readFile(startupFile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + await fs.writeFile(startupFile, removeStartupCode(content, regionStart, regionEnd)); + traceInfo(`Removed activation from CMD activation file at: ${startupFile}`); + } else { + traceInfo(`CMD activation file at ${startupFile} does not contain activation code`); + } + } catch (err) { + traceVerbose(`Failed to remove CMD activation file content`, err); + return false; + } + } + return true; +} + +export class CmdStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'Command Prompt'; + public readonly shellType: string = ShellConstants.CMD; + + async isSetup(): Promise { + const isInstalled = await isCmdInstalled(); + if (!isInstalled) { + traceVerbose('CMD is not installed or not on Windows'); + return ShellSetupState.NotInstalled; + } + + try { + const cmdFiles = await getCmdFilePaths(); + const isSetup = await isCmdStartupSetup(cmdFiles, CMD_ENV_KEY); + return isSetup; + } catch (err) { + traceError('Failed to check if CMD startup is setup', err); + return ShellSetupState.NotSetup; + } + } + + async setupScripts(): Promise { + const isInstalled = await isCmdInstalled(); + if (!isInstalled) { + traceVerbose('CMD is not installed or not on Windows'); + return ShellScriptEditState.NotInstalled; + } + + try { + const cmdFiles = await getCmdFilePaths(); + const success = await setupCmdStartup(cmdFiles, CMD_ENV_KEY); + return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup CMD startup', err); + return ShellScriptEditState.NotEdited; + } + } + + async teardownScripts(): Promise { + const isInstalled = await isCmdInstalled(); + if (!isInstalled) { + traceVerbose('CMD is not installed or not on Windows'); + return ShellScriptEditState.NotInstalled; + } + + try { + const { startupFile } = await getCmdFilePaths(); + const success = await removeCmdStartup(startupFile, CMD_ENV_KEY); + return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to remove CMD startup', err); + return ShellScriptEditState.NotEdited; + } + } + + clearCache(): Promise { + return Promise.resolve(); + } +} diff --git a/src/features/terminal/shells/common/editUtils.ts b/src/features/terminal/shells/common/editUtils.ts new file mode 100644 index 0000000..ea60638 --- /dev/null +++ b/src/features/terminal/shells/common/editUtils.ts @@ -0,0 +1,78 @@ +import { isWindows } from '../../../../common/utils/platformUtils'; + +export function hasStartupCode(content: string, start: string, end: string, keys: string[]): boolean { + const normalizedContent = content.replace(/\r\n/g, '\n'); + const startIndex = normalizedContent.indexOf(start); + const endIndex = normalizedContent.indexOf(end); + if (startIndex === -1 || endIndex === -1 || startIndex >= endIndex) { + return false; + } + const contentBetween = normalizedContent.substring(startIndex + start.length, endIndex).trim(); + return contentBetween.length > 0 && keys.every((key) => contentBetween.includes(key)); +} + +function getLineEndings(content: string): string { + if (content.includes('\r\n')) { + return '\r\n'; + } else if (content.includes('\n')) { + return '\n'; + } + return isWindows() ? '\r\n' : '\n'; +} + +export function insertStartupCode(content: string, start: string, end: string, code: string): string { + let lineEnding = getLineEndings(content); + const normalizedContent = content.replace(/\r\n/g, '\n'); + + const startIndex = normalizedContent.indexOf(start); + const endIndex = normalizedContent.indexOf(end); + + let result: string; + if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) { + result = + normalizedContent.substring(0, startIndex + start.length) + + '\n' + + code + + '\n' + + normalizedContent.substring(endIndex); + } else if (startIndex !== -1) { + result = normalizedContent.substring(0, startIndex + start.length) + '\n' + code + '\n' + end + '\n'; + } else { + result = normalizedContent + '\n' + start + '\n' + code + '\n' + end + '\n'; + } + + if (lineEnding === '\r\n') { + result = result.replace(/\n/g, '\r\n'); + } + return result; +} + +export function removeStartupCode(content: string, start: string, end: string): string { + let lineEnding = getLineEndings(content); + const normalizedContent = content.replace(/\r\n/g, '\n'); + + const startIndex = normalizedContent.indexOf(start); + const endIndex = normalizedContent.indexOf(end); + + if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) { + const before = normalizedContent.substring(0, startIndex); + const after = normalizedContent.substring(endIndex + end.length); + + let result: string; + if (before === '') { + result = after.startsWith('\n') ? after.substring(1) : after; + } else if (after === '' || after === '\n') { + result = before.endsWith('\n') ? before.substring(0, before.length - 1) : before; + } else if (after.startsWith('\n') && before.endsWith('\n')) { + result = before + after.substring(1); + } else { + result = before + after; + } + + if (lineEnding === '\r\n') { + result = result.replace(/\n/g, '\r\n'); + } + return result; + } + return content; +} diff --git a/src/features/terminal/shells/common/shellUtils.ts b/src/features/terminal/shells/common/shellUtils.ts new file mode 100644 index 0000000..381be88 --- /dev/null +++ b/src/features/terminal/shells/common/shellUtils.ts @@ -0,0 +1,163 @@ +import { PythonCommandRunConfiguration, PythonEnvironment } from '../../../../api'; +import { traceInfo } from '../../../../common/logging'; +import { getGlobalPersistentState } from '../../../../common/persistentState'; +import { sleep } from '../../../../common/utils/asyncUtils'; +import { isWindows } from '../../../../common/utils/platformUtils'; +import { activeTerminalShellIntegration } from '../../../../common/window.apis'; +import { getConfiguration } from '../../../../common/workspace.apis'; +import { ShellConstants } from '../../../common/shellConstants'; +import { quoteArgs } from '../../../execution/execUtils'; +import { SHELL_INTEGRATION_POLL_INTERVAL, SHELL_INTEGRATION_TIMEOUT } from '../../utils'; + +export const SHELL_INTEGRATION_STATE_KEY = 'shellIntegration.enabled'; + +function getCommandAsString(command: PythonCommandRunConfiguration[], shell: string, delimiter: string): string { + const parts = []; + for (const cmd of command) { + const args = cmd.args ?? []; + parts.push(quoteArgs([normalizeShellPath(cmd.executable, shell), ...args]).join(' ')); + } + if (shell === ShellConstants.PWSH) { + if (parts.length === 1) { + return parts[0]; + } + return parts.map((p) => `(${p})`).join(` ${delimiter} `); + } + return parts.join(` ${delimiter} `); +} + +export function getShellCommandAsString(shell: string, command: PythonCommandRunConfiguration[]): string { + switch (shell) { + case ShellConstants.PWSH: + return getCommandAsString(command, shell, ';'); + case ShellConstants.NU: + return getCommandAsString(command, shell, ';'); + case ShellConstants.FISH: + return getCommandAsString(command, shell, '; and'); + case ShellConstants.BASH: + case ShellConstants.SH: + case ShellConstants.ZSH: + + case ShellConstants.CMD: + case ShellConstants.GITBASH: + default: + return getCommandAsString(command, shell, '&&'); + } +} + +export function normalizeShellPath(filePath: string, shellType?: string): string { + if (isWindows() && shellType) { + if (shellType.toLowerCase() === ShellConstants.GITBASH || shellType.toLowerCase() === 'git-bash') { + return filePath.replace(/\\/g, '/').replace(/^\/([a-zA-Z])/, '$1:'); + } + } + return filePath; +} +export function getShellActivationCommand( + shell: string, + environment: PythonEnvironment, +): PythonCommandRunConfiguration[] | undefined { + let activation: PythonCommandRunConfiguration[] | undefined; + if (environment.execInfo?.shellActivation) { + activation = environment.execInfo.shellActivation.get(shell); + if (!activation) { + activation = environment.execInfo.shellActivation.get('unknown'); + } + } + + if (!activation) { + activation = environment.execInfo?.activation; + } + + return activation; +} +export function getShellDeactivationCommand( + shell: string, + environment: PythonEnvironment, +): PythonCommandRunConfiguration[] | undefined { + let deactivation: PythonCommandRunConfiguration[] | undefined; + if (environment.execInfo?.shellDeactivation) { + deactivation = environment.execInfo.shellDeactivation.get(shell); + if (!deactivation) { + deactivation = environment.execInfo.shellDeactivation.get('unknown'); + } + } + + if (!deactivation) { + deactivation = environment.execInfo?.deactivation; + } + + return deactivation; +} + +export const PROFILE_TAG_START = '###PATH_START###'; +export const PROFILE_TAG_END = '###PATH_END###'; +export function extractProfilePath(content: string): string | undefined { + // Extract only the part between the tags + const profilePathRegex = new RegExp(`${PROFILE_TAG_START}\\r?\\n(.*?)\\r?\\n${PROFILE_TAG_END}`, 's'); + const match = content?.match(profilePathRegex); + + if (match && match[1]) { + const extractedPath = match[1].trim(); + return extractedPath; + } + return undefined; +} + +export async function shellIntegrationForActiveTerminal(name: string, profile?: string): Promise { + let hasShellIntegration = activeTerminalShellIntegration(); + let timeout = 0; + + while (!hasShellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) { + await sleep(SHELL_INTEGRATION_POLL_INTERVAL); + timeout += SHELL_INTEGRATION_POLL_INTERVAL; + hasShellIntegration = activeTerminalShellIntegration(); + } + + if (hasShellIntegration) { + traceInfo( + `SHELL: Shell integration is available on your active terminal, with name ${name} and profile ${profile}. Python activate scripts will be evaluated at shell integration level, except in WSL.`, + ); + + // Update persistent storage to reflect that shell integration is available + const persistentState = await getGlobalPersistentState(); + await persistentState.set(SHELL_INTEGRATION_STATE_KEY, true); + + return true; + } + return false; +} + +export function isWsl(): boolean { + // WSL sets these environment variables + return !!(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP || process.env.WSLENV); +} + +export async function getShellIntegrationEnabledCache(): Promise { + const persistentState = await getGlobalPersistentState(); + const shellIntegrationInspect = + getConfiguration('terminal.integrated').inspect('shellIntegration.enabled'); + + let shellIntegrationEnabled = true; + if (shellIntegrationInspect) { + // Priority: workspaceFolder > workspace > globalRemoteValue > globalLocalValue > global > default + const inspectValue = shellIntegrationInspect as Record; + + if (shellIntegrationInspect.workspaceFolderValue !== undefined) { + shellIntegrationEnabled = shellIntegrationInspect.workspaceFolderValue; + } else if (shellIntegrationInspect.workspaceValue !== undefined) { + shellIntegrationEnabled = shellIntegrationInspect.workspaceValue; + } else if ('globalRemoteValue' in shellIntegrationInspect && inspectValue.globalRemoteValue !== undefined) { + shellIntegrationEnabled = inspectValue.globalRemoteValue as boolean; + } else if ('globalLocalValue' in shellIntegrationInspect && inspectValue.globalLocalValue !== undefined) { + shellIntegrationEnabled = inspectValue.globalLocalValue as boolean; + } else if (shellIntegrationInspect.globalValue !== undefined) { + shellIntegrationEnabled = shellIntegrationInspect.globalValue; + } else if (shellIntegrationInspect.defaultValue !== undefined) { + shellIntegrationEnabled = shellIntegrationInspect.defaultValue; + } + } + + await persistentState.set(SHELL_INTEGRATION_STATE_KEY, shellIntegrationEnabled); + return shellIntegrationEnabled; +} diff --git a/src/features/terminal/shells/fish/fishConstants.ts b/src/features/terminal/shells/fish/fishConstants.ts new file mode 100644 index 0000000..84b598c --- /dev/null +++ b/src/features/terminal/shells/fish/fishConstants.ts @@ -0,0 +1,3 @@ +export const FISH_ENV_KEY = 'VSCODE_PYTHON_FISH_ACTIVATE'; +export const FISH_OLD_ENV_KEY = 'VSCODE_FISH_ACTIVATE'; +export const FISH_SCRIPT_VERSION = '0.1.1'; diff --git a/src/features/terminal/shells/fish/fishEnvs.ts b/src/features/terminal/shells/fish/fishEnvs.ts new file mode 100644 index 0000000..665385c --- /dev/null +++ b/src/features/terminal/shells/fish/fishEnvs.ts @@ -0,0 +1,50 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../../api'; +import { traceError } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; +import { ShellEnvsProvider } from '../startupProvider'; +import { FISH_ENV_KEY } from './fishConstants'; + +export class FishEnvsProvider implements ShellEnvsProvider { + readonly shellType: string = ShellConstants.FISH; + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const fishActivation = getShellActivationCommand(this.shellType, env); + if (fishActivation) { + const command = getShellCommandAsString(this.shellType, fishActivation); + const v = collection.get(FISH_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(FISH_ENV_KEY, command); + } else { + collection.delete(FISH_ENV_KEY); + } + } catch (err) { + traceError('Failed to update Fish environment variables', err); + collection.delete(FISH_ENV_KEY); + } + } + + removeEnvVariables(envCollection: EnvironmentVariableCollection): void { + envCollection.delete(FISH_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[FISH_ENV_KEY, undefined]]); + } + + try { + const fishActivation = getShellActivationCommand(this.shellType, env); + if (fishActivation) { + return new Map([[FISH_ENV_KEY, getShellCommandAsString(this.shellType, fishActivation)]]); + } + return undefined; + } catch (err) { + traceError('Failed to get Fish environment variables', err); + return undefined; + } + } +} diff --git a/src/features/terminal/shells/fish/fishStartup.ts b/src/features/terminal/shells/fish/fishStartup.ts new file mode 100644 index 0000000..40de3e9 --- /dev/null +++ b/src/features/terminal/shells/fish/fishStartup.ts @@ -0,0 +1,166 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import which from 'which'; + +import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; +import { getShellIntegrationEnabledCache, isWsl, shellIntegrationForActiveTerminal } from '../common/shellUtils'; +import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; +import { FISH_ENV_KEY, FISH_OLD_ENV_KEY, FISH_SCRIPT_VERSION } from './fishConstants'; + +async function isFishInstalled(): Promise { + try { + await which('fish'); + return true; + } catch { + traceVerbose('Fish is not installed or not found in PATH'); + return false; + } +} + +async function getFishProfile(): Promise { + const homeDir = os.homedir(); + // Fish configuration is typically at ~/.config/fish/config.fish + const profilePath = path.join(homeDir, '.config', 'fish', 'config.fish'); + traceInfo(`SHELL: fish profile found at: ${profilePath}`); + return profilePath; +} + +const regionStart = '# >>> vscode python'; +const regionEnd = '# <<< vscode python'; + +function getActivationContent(key: string): string { + const lineSep = '\n'; + return [ + `# version: ${FISH_SCRIPT_VERSION}`, + `if not set -q VSCODE_PYTHON_AUTOACTIVATE_GUARD`, + ` set -gx VSCODE_PYTHON_AUTOACTIVATE_GUARD 1`, + ` if test "$TERM_PROGRAM" = "vscode"; and set -q ${key}`, + ` eval $${key}`, + ` end`, + `end`, + ].join(lineSep); +} + +async function isStartupSetup(profilePath: string, key: string): Promise { + if (await fs.pathExists(profilePath)) { + const content = await fs.readFile(profilePath, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + traceInfo(`SHELL: fish already contains activation code: ${profilePath}`); + return true; + } + } + traceInfo(`SHELL: fish does not contain activation code: ${profilePath}`); + return false; +} + +async function setupStartup(profilePath: string, key: string): Promise { + try { + const shellIntegrationEnabled = await getShellIntegrationEnabledCache(); + if ((shellIntegrationEnabled || (await shellIntegrationForActiveTerminal('fish', profilePath))) && !isWsl()) { + removeFishStartup(profilePath, key); + return true; + } + const activationContent = getActivationContent(key); + await fs.mkdirp(path.dirname(profilePath)); + + if (await fs.pathExists(profilePath)) { + const content = await fs.readFile(profilePath, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + traceInfo(`SHELL: Fish profile at ${profilePath} already contains activation code`); + } else { + await fs.writeFile(profilePath, insertStartupCode(content, regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Updated existing fish profile at: ${profilePath}\n${activationContent}`); + } + } else { + await fs.writeFile(profilePath, insertStartupCode('', regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Created new fish profile at: ${profilePath}\n${activationContent}`); + } + return true; + } catch (err) { + traceVerbose(`Failed to setup fish startup`, err); + return false; + } +} + +async function removeFishStartup(profilePath: string, key: string): Promise { + if (!(await fs.pathExists(profilePath))) { + return true; + } + + try { + const content = await fs.readFile(profilePath, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + await fs.writeFile(profilePath, removeStartupCode(content, regionStart, regionEnd)); + traceInfo(`Removed activation from fish profile at: ${profilePath}, for key: ${key}`); + } + return true; + } catch (err) { + traceVerbose(`Failed to remove fish startup, for key: ${key}`, err); + return false; + } +} + +export class FishStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'fish'; + public readonly shellType: string = ShellConstants.FISH; + + async isSetup(): Promise { + const isInstalled = await isFishInstalled(); + if (!isInstalled) { + traceVerbose('Fish is not installed'); + return ShellSetupState.NotInstalled; + } + + try { + const fishProfile = await getFishProfile(); + const isSetup = await isStartupSetup(fishProfile, FISH_ENV_KEY); + return isSetup ? ShellSetupState.Setup : ShellSetupState.NotSetup; + } catch (err) { + traceError('Failed to check if Fish startup is setup', err); + return ShellSetupState.NotSetup; + } + } + + async setupScripts(): Promise { + const isInstalled = await isFishInstalled(); + if (!isInstalled) { + traceVerbose('Fish is not installed'); + return ShellScriptEditState.NotInstalled; + } + + try { + const fishProfile = await getFishProfile(); + const success = await setupStartup(fishProfile, FISH_ENV_KEY); + return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to setup Fish startup', err); + return ShellScriptEditState.NotEdited; + } + } + + async teardownScripts(): Promise { + const isInstalled = await isFishInstalled(); + if (!isInstalled) { + traceVerbose('Fish is not installed'); + return ShellScriptEditState.NotInstalled; + } + + try { + const fishProfile = await getFishProfile(); + // Remove old environment variable if it exists + await removeFishStartup(fishProfile, FISH_OLD_ENV_KEY); + const success = await removeFishStartup(fishProfile, FISH_ENV_KEY); + return success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited; + } catch (err) { + traceError('Failed to remove Fish startup', err); + return ShellScriptEditState.NotEdited; + } + } + + clearCache(): Promise { + return Promise.resolve(); + } +} diff --git a/src/features/terminal/shells/providers.ts b/src/features/terminal/shells/providers.ts new file mode 100644 index 0000000..03f0128 --- /dev/null +++ b/src/features/terminal/shells/providers.ts @@ -0,0 +1,45 @@ +import { isWindows } from '../../../common/utils/platformUtils'; +import { ShellConstants } from '../../common/shellConstants'; +import { BashEnvsProvider, ZshEnvsProvider } from './bash/bashEnvs'; +import { BashStartupProvider, GitBashStartupProvider, ZshStartupProvider } from './bash/bashStartup'; +import { CmdEnvsProvider } from './cmd/cmdEnvs'; +import { CmdStartupProvider } from './cmd/cmdStartup'; +import { FishEnvsProvider } from './fish/fishEnvs'; +import { FishStartupProvider } from './fish/fishStartup'; +import { PowerShellEnvsProvider } from './pwsh/pwshEnvs'; +import { PwshStartupProvider } from './pwsh/pwshStartup'; +import { ShellEnvsProvider, ShellStartupScriptProvider } from './startupProvider'; + +export function createShellStartupProviders(): ShellStartupScriptProvider[] { + if (isWindows()) { + return [ + // PowerShell classic is the default on Windows, so it is included here explicitly. + // pwsh is the new PowerShell Core, which is cross-platform and preferred. + new PwshStartupProvider([ShellConstants.PWSH, 'powershell']), + new GitBashStartupProvider(), + new CmdStartupProvider(), + ]; + } + return [ + new PwshStartupProvider([ShellConstants.PWSH]), + new BashStartupProvider(), + new FishStartupProvider(), + new ZshStartupProvider(), + ]; +} + +export function createShellEnvProviders(): ShellEnvsProvider[] { + if (isWindows()) { + return [new PowerShellEnvsProvider(), new BashEnvsProvider(ShellConstants.GITBASH), new CmdEnvsProvider()]; + } + return [ + new PowerShellEnvsProvider(), + new BashEnvsProvider(ShellConstants.BASH), + new FishEnvsProvider(), + new ZshEnvsProvider(), + ]; +} + +export async function clearShellProfileCache(providers: ShellStartupScriptProvider[]): Promise { + await Promise.all(providers.map((provider) => provider.clearCache())); +} diff --git a/src/features/terminal/shells/pwsh/pwshConstants.ts b/src/features/terminal/shells/pwsh/pwshConstants.ts new file mode 100644 index 0000000..bc49013 --- /dev/null +++ b/src/features/terminal/shells/pwsh/pwshConstants.ts @@ -0,0 +1,3 @@ +export const POWERSHELL_ENV_KEY = 'VSCODE_PYTHON_PWSH_ACTIVATE'; +export const POWERSHELL_OLD_ENV_KEY = 'VSCODE_PWSH_ACTIVATE'; +export const PWSH_SCRIPT_VERSION = '0.1.1'; diff --git a/src/features/terminal/shells/pwsh/pwshEnvs.ts b/src/features/terminal/shells/pwsh/pwshEnvs.ts new file mode 100644 index 0000000..5d598b0 --- /dev/null +++ b/src/features/terminal/shells/pwsh/pwshEnvs.ts @@ -0,0 +1,51 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../../api'; +import { traceError } from '../../../../common/logging'; +import { ShellConstants } from '../../../common/shellConstants'; +import { getShellActivationCommand, getShellCommandAsString } from '../common/shellUtils'; +import { ShellEnvsProvider } from '../startupProvider'; +import { POWERSHELL_ENV_KEY } from './pwshConstants'; + +export class PowerShellEnvsProvider implements ShellEnvsProvider { + public readonly shellType: string = ShellConstants.PWSH; + + updateEnvVariables(collection: EnvironmentVariableCollection, env: PythonEnvironment): void { + try { + const pwshActivation = getShellActivationCommand(this.shellType, env); + if (pwshActivation) { + const command = getShellCommandAsString(this.shellType, pwshActivation); + const v = collection.get(POWERSHELL_ENV_KEY); + if (v?.value === command) { + return; + } + collection.replace(POWERSHELL_ENV_KEY, command); + } else { + collection.delete(POWERSHELL_ENV_KEY); + } + } catch (err) { + traceError('Failed to update PowerShell environment variables', err); + collection.delete(POWERSHELL_ENV_KEY); + } + } + + removeEnvVariables(envCollection: EnvironmentVariableCollection): void { + envCollection.delete(POWERSHELL_ENV_KEY); + } + + getEnvVariables(env?: PythonEnvironment): Map | undefined { + if (!env) { + return new Map([[POWERSHELL_ENV_KEY, undefined]]); + } + + try { + const pwshActivation = getShellActivationCommand(this.shellType, env); + if (pwshActivation) { + return new Map([[POWERSHELL_ENV_KEY, getShellCommandAsString(this.shellType, pwshActivation)]]); + } + return undefined; + } catch (err) { + traceError('Failed to get PowerShell environment variables', err); + return undefined; + } + } +} diff --git a/src/features/terminal/shells/pwsh/pwshStartup.ts b/src/features/terminal/shells/pwsh/pwshStartup.ts new file mode 100644 index 0000000..45bb33d --- /dev/null +++ b/src/features/terminal/shells/pwsh/pwshStartup.ts @@ -0,0 +1,357 @@ +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import which from 'which'; +import { traceError, traceInfo, traceVerbose } from '../../../../common/logging'; +import { isWindows } from '../../../../common/utils/platformUtils'; +import { ShellScriptEditState, ShellSetupState, ShellStartupScriptProvider } from '../startupProvider'; +import { runCommand } from '../utils'; + +import assert from 'assert'; +import { getWorkspacePersistentState } from '../../../../common/persistentState'; +import { ShellConstants } from '../../../common/shellConstants'; +import { hasStartupCode, insertStartupCode, removeStartupCode } from '../common/editUtils'; +import { + extractProfilePath, + getShellIntegrationEnabledCache, + isWsl, + PROFILE_TAG_END, + PROFILE_TAG_START, + shellIntegrationForActiveTerminal, +} from '../common/shellUtils'; +import { POWERSHELL_ENV_KEY, POWERSHELL_OLD_ENV_KEY, PWSH_SCRIPT_VERSION } from './pwshConstants'; + +const PWSH_PROFILE_PATH_CACHE_KEY = 'PWSH_PROFILE_PATH_CACHE'; +const PS5_PROFILE_PATH_CACHE_KEY = 'PS5_PROFILE_PATH_CACHE'; +let pwshProfilePath: string | undefined; +let ps5ProfilePath: string | undefined; +function clearPwshCache() { + ps5ProfilePath = undefined; + pwshProfilePath = undefined; +} + +async function setProfilePathCache(shell: 'powershell' | 'pwsh', profilePath: string): Promise { + const state = await getWorkspacePersistentState(); + if (shell === 'powershell') { + ps5ProfilePath = profilePath; + await state.set(PS5_PROFILE_PATH_CACHE_KEY, profilePath); + } else { + pwshProfilePath = profilePath; + await state.set(PWSH_PROFILE_PATH_CACHE_KEY, profilePath); + } +} + +function getProfilePathCache(shell: 'powershell' | 'pwsh'): string | undefined { + if (shell === 'powershell') { + return ps5ProfilePath; + } else { + return pwshProfilePath; + } +} + +async function isPowerShellInstalled(shell: string): Promise { + try { + await which(shell); + return true; + } catch { + traceVerbose(`${shell} is not installed`); + return false; + } +} + +/** + * Detects the major version of PowerShell by executing a version query command. + * This helps with debugging activation issues since PowerShell 5.x and 7+ have different behaviors. + * @param shell The PowerShell executable name ('powershell' for Windows PowerShell or 'pwsh' for PowerShell Core/7+) + * @returns Promise resolving to the major version number as a string, or undefined if detection fails + */ +async function getPowerShellVersion(shell: 'powershell' | 'pwsh'): Promise { + try { + const command = `${shell} -c '\$PSVersionTable.PSVersion.Major'`; + const versionOutput = await runCommand(command); + if (versionOutput && !isNaN(Number(versionOutput))) { + return versionOutput; + } + traceVerbose(`Failed to parse PowerShell version from output: ${versionOutput}`); + return undefined; + } catch (err) { + traceVerbose(`Failed to get PowerShell version for ${shell}`, err); + return undefined; + } +} + +async function getProfileForShell(shell: 'powershell' | 'pwsh'): Promise { + const cachedPath = getProfilePathCache(shell); + if (cachedPath) { + traceInfo(`SHELL: ${shell} profile path from cache: ${cachedPath}`); + return cachedPath; + } + + try { + const content = await runCommand( + isWindows() + ? `${shell} -Command "Write-Output '${PROFILE_TAG_START}'; Write-Output $profile; Write-Output '${PROFILE_TAG_END}'"` + : `${shell} -Command "Write-Output '${PROFILE_TAG_START}'; Write-Output \\$profile; Write-Output '${PROFILE_TAG_END}'"`, + ); + + if (content) { + const profilePath = extractProfilePath(content); + if (profilePath) { + setProfilePathCache(shell, profilePath); + traceInfo(`SHELL: ${shell} profile found at: ${profilePath}`); + return profilePath; + } + } + } catch (err) { + traceError(`${shell} failed to get profile path`, err); + } + + let profile: string; + if (isWindows()) { + if (shell === 'powershell') { + profile = path.join( + process.env.USERPROFILE || os.homedir(), + 'Documents', + 'WindowsPowerShell', + 'Microsoft.PowerShell_profile.ps1', + ); + } else { + profile = path.join( + process.env.USERPROFILE || os.homedir(), + 'Documents', + 'PowerShell', + 'Microsoft.PowerShell_profile.ps1', + ); + } + } else { + profile = path.join( + process.env.HOME || os.homedir(), + '.config', + 'powershell', + 'Microsoft.PowerShell_profile.ps1', + ); + } + traceInfo(`SHELL: ${shell} profile not found, using default path: ${profile}`); + return profile; +} + +const regionStart = '#region vscode python'; +const regionEnd = '#endregion vscode python'; +function getActivationContent(): string { + const lineSep = isWindows() ? '\r\n' : '\n'; + const activationContent = [ + `#version: ${PWSH_SCRIPT_VERSION}`, + `if (-not $env:VSCODE_PYTHON_AUTOACTIVATE_GUARD) {`, + ` $env:VSCODE_PYTHON_AUTOACTIVATE_GUARD = '1'`, + ` if (($env:TERM_PROGRAM -eq 'vscode') -and ($null -ne $env:${POWERSHELL_ENV_KEY})) {`, + ' try {', + ` Invoke-Expression $env:${POWERSHELL_ENV_KEY}`, + ' } catch {', + ` $psVersion = $PSVersionTable.PSVersion.Major`, + ` Write-Error "Failed to activate Python environment (PowerShell $psVersion): $_" -ErrorAction Continue`, + ' }', + ' }', + '}', + ].join(lineSep); + return activationContent; +} + +async function isPowerShellStartupSetup(shell: string, profile: string): Promise { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [POWERSHELL_ENV_KEY])) { + traceInfo(`SHELL: ${shell} already contains activation code: ${profile}`); + return true; + } + } + traceInfo(`SHELL: ${shell} does not contain activation code: ${profile}`); + return false; +} + +async function setupPowerShellStartup(shell: string, profile: string): Promise { + const shellIntegrationEnabled = await getShellIntegrationEnabledCache(); + + if ((shellIntegrationEnabled || (await shellIntegrationForActiveTerminal(shell, profile))) && !isWsl()) { + removePowerShellStartup(shell, profile, POWERSHELL_OLD_ENV_KEY); + removePowerShellStartup(shell, profile, POWERSHELL_ENV_KEY); + return true; + } + const activationContent = getActivationContent(); + + try { + if (await fs.pathExists(profile)) { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [POWERSHELL_ENV_KEY])) { + traceInfo(`SHELL: ${shell} already contains activation code: ${profile}`); + } else { + await fs.writeFile(profile, insertStartupCode(content, regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Updated existing ${shell} profile at: ${profile}\r\n${activationContent}`); + } + } else { + await fs.mkdirp(path.dirname(profile)); + await fs.writeFile(profile, insertStartupCode('', regionStart, regionEnd, activationContent)); + traceInfo(`SHELL: Created new ${shell} profile at: ${profile}\r\n${activationContent}`); + } + return true; + } catch (err) { + traceError(`Failed to setup ${shell} startup`, err); + return false; + } +} + +async function removePowerShellStartup(shell: string, profile: string, key: string): Promise { + if (!(await fs.pathExists(profile))) { + return true; + } + + try { + const content = await fs.readFile(profile, 'utf8'); + if (hasStartupCode(content, regionStart, regionEnd, [key])) { + await fs.writeFile(profile, removeStartupCode(content, regionStart, regionEnd)); + traceInfo(`SHELL: Removed activation from ${shell} profile at: ${profile}, for key: ${key}`); + } else { + traceInfo(`SHELL: No activation code found in ${shell} profile at: ${profile}, for key: ${key}`); + } + return true; + } catch (err) { + traceError(`SHELL: Failed to remove startup code for ${shell} profile at: ${profile}, for key: ${key}`, err); + return false; + } +} + +type PowerShellType = 'powershell' | 'pwsh'; + +export class PwshStartupProvider implements ShellStartupScriptProvider { + public readonly name: string = 'PowerShell'; + public readonly shellType: string = ShellConstants.PWSH; + + private _isPwshInstalled: boolean | undefined; + private _isPs5Installed: boolean | undefined; + private _supportedShells: PowerShellType[]; + + constructor(supportedShells: PowerShellType[]) { + assert(supportedShells.length > 0, 'At least one PowerShell shell must be supported'); + this._supportedShells = supportedShells; + } + + private async checkInstallations(): Promise> { + const results = new Map(); + + await Promise.all( + this._supportedShells.map(async (shell) => { + if (shell === 'pwsh' && this._isPwshInstalled !== undefined) { + results.set(shell, this._isPwshInstalled); + } else if (shell === 'powershell' && this._isPs5Installed !== undefined) { + results.set(shell, this._isPs5Installed); + } else { + const isInstalled = await isPowerShellInstalled(shell); + if (isInstalled) { + // Log PowerShell version for debugging activation issues + const version = await getPowerShellVersion(shell); + const versionText = version ? ` (version ${version})` : ' (version unknown)'; + traceInfo(`SHELL: ${shell} is installed${versionText}`); + } + if (shell === 'pwsh') { + this._isPwshInstalled = isInstalled; + } else { + this._isPs5Installed = isInstalled; + } + results.set(shell, isInstalled); + } + }), + ); + + return results; + } + + async isSetup(): Promise { + const installations = await this.checkInstallations(); + + if (Array.from(installations.values()).every((installed) => !installed)) { + return ShellSetupState.NotInstalled; + } + + const results: ShellSetupState[] = []; + for (const [shell, installed] of installations.entries()) { + if (!installed) { + continue; + } + + try { + const profile = await getProfileForShell(shell); + const isSetup = await isPowerShellStartupSetup(shell, profile); + results.push(isSetup ? ShellSetupState.Setup : ShellSetupState.NotSetup); + } catch (err) { + traceError(`Failed to check if ${shell} startup is setup`, err); + results.push(ShellSetupState.NotSetup); + } + } + + if (results.includes(ShellSetupState.NotSetup)) { + return ShellSetupState.NotSetup; + } + + return ShellSetupState.Setup; + } + + async setupScripts(): Promise { + const installations = await this.checkInstallations(); + + if (Array.from(installations.values()).every((installed) => !installed)) { + return ShellScriptEditState.NotInstalled; + } + + const anyEdited = []; + for (const [shell, installed] of installations.entries()) { + if (!installed) { + continue; + } + + try { + const profile = await getProfileForShell(shell); + const success = await setupPowerShellStartup(shell, profile); + anyEdited.push(success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited); + } catch (err) { + traceError(`Failed to setup ${shell} startup`, err); + } + } + return anyEdited.every((state) => state === ShellScriptEditState.Edited) + ? ShellScriptEditState.Edited + : ShellScriptEditState.NotEdited; + } + + async teardownScripts(): Promise { + const installations = await this.checkInstallations(); + + if (Array.from(installations.values()).every((installed) => !installed)) { + return ShellScriptEditState.NotInstalled; + } + + const anyEdited = []; + for (const [shell, installed] of installations.entries()) { + if (!installed) { + continue; + } + + try { + const profile = await getProfileForShell(shell); + // Remove old environment variable if it exists + await removePowerShellStartup(shell, profile, POWERSHELL_OLD_ENV_KEY); + const success = await removePowerShellStartup(shell, profile, POWERSHELL_ENV_KEY); + anyEdited.push(success ? ShellScriptEditState.Edited : ShellScriptEditState.NotEdited); + } catch (err) { + traceError(`Failed to remove ${shell} startup`, err); + } + } + return anyEdited.every((state) => state === ShellScriptEditState.Edited) + ? ShellScriptEditState.Edited + : ShellScriptEditState.NotEdited; + } + + async clearCache(): Promise { + clearPwshCache(); + // Reset installation check cache as well + this._isPwshInstalled = undefined; + this._isPs5Installed = undefined; + } +} diff --git a/src/features/terminal/shells/startupProvider.ts b/src/features/terminal/shells/startupProvider.ts new file mode 100644 index 0000000..5d118ca --- /dev/null +++ b/src/features/terminal/shells/startupProvider.ts @@ -0,0 +1,30 @@ +import { EnvironmentVariableCollection } from 'vscode'; +import { PythonEnvironment } from '../../../api'; + +export enum ShellSetupState { + NotSetup, + Setup, + NotInstalled, +} + +export enum ShellScriptEditState { + NotEdited, + Edited, + NotInstalled, +} + +export interface ShellStartupScriptProvider { + name: string; + readonly shellType: string; + isSetup(): Promise; + setupScripts(): Promise; + teardownScripts(): Promise; + clearCache(): Promise; +} + +export interface ShellEnvsProvider { + readonly shellType: string; + updateEnvVariables(envVars: EnvironmentVariableCollection, env: PythonEnvironment): void; + removeEnvVariables(envVars: EnvironmentVariableCollection): void; + getEnvVariables(env?: PythonEnvironment): Map | undefined; +} diff --git a/src/features/terminal/shells/utils.ts b/src/features/terminal/shells/utils.ts new file mode 100644 index 0000000..6506ed1 --- /dev/null +++ b/src/features/terminal/shells/utils.ts @@ -0,0 +1,18 @@ +import * as cp from 'child_process'; +import { traceError, traceInfo } from '../../../common/logging'; +import { StopWatch } from '../../../common/stopWatch'; + +export async function runCommand(command: string): Promise { + return new Promise((resolve) => { + const timer = new StopWatch(); + cp.exec(command, (err, stdout) => { + if (err) { + traceError(`Error running command: ${command} (${timer.elapsedTime})`, err); + resolve(undefined); + } else { + traceInfo(`Ran ${command} in ${timer.elapsedTime}`); + resolve(stdout?.trim()); + } + }); + }); +} diff --git a/src/features/terminal/terminalActivationState.ts b/src/features/terminal/terminalActivationState.ts new file mode 100644 index 0000000..c022088 --- /dev/null +++ b/src/features/terminal/terminalActivationState.ts @@ -0,0 +1,287 @@ +import { + Disposable, + Event, + EventEmitter, + Terminal, + TerminalShellExecutionEndEvent, + TerminalShellExecutionStartEvent, + TerminalShellIntegration, +} from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { traceError, traceInfo, traceVerbose } from '../../common/logging'; +import { onDidEndTerminalShellExecution, onDidStartTerminalShellExecution } from '../../common/window.apis'; +import { getActivationCommand, getDeactivationCommand } from '../common/activation'; +import { isTaskTerminal } from './utils'; + +export interface DidChangeTerminalActivationStateEvent { + terminal: Terminal; + environment: PythonEnvironment; + activated: boolean; +} + +export interface TerminalActivation { + isActivated(terminal: Terminal, environment?: PythonEnvironment): boolean; + activate(terminal: Terminal, environment: PythonEnvironment): Promise; + deactivate(terminal: Terminal): Promise; + onDidChangeTerminalActivationState: Event; +} + +export interface TerminalEnvironment { + getEnvironment(terminal: Terminal): PythonEnvironment | undefined; +} + +export interface TerminalActivationInternal extends TerminalActivation, TerminalEnvironment, Disposable { + updateActivationState(terminal: Terminal, environment: PythonEnvironment, activated: boolean): void; +} + +export class TerminalActivationImpl implements TerminalActivationInternal { + private disposables: Disposable[] = []; + + private onTerminalShellExecutionStartEmitter = new EventEmitter(); + private onTerminalShellExecutionStart = this.onTerminalShellExecutionStartEmitter.event; + + private onTerminalShellExecutionEndEmitter = new EventEmitter(); + private onTerminalShellExecutionEnd = this.onTerminalShellExecutionEndEmitter.event; + + private onDidChangeTerminalActivationStateEmitter = new EventEmitter(); + onDidChangeTerminalActivationState = this.onDidChangeTerminalActivationStateEmitter.event; + + private onTerminalClosedEmitter = new EventEmitter(); + private onTerminalClosed = this.onTerminalClosedEmitter.event; + + private activatedTerminals = new Map(); + private activatingTerminals = new Map>(); + private deactivatingTerminals = new Map>(); + + constructor() { + this.disposables.push( + this.onDidChangeTerminalActivationStateEmitter, + this.onTerminalShellExecutionStartEmitter, + this.onTerminalShellExecutionEndEmitter, + this.onTerminalClosedEmitter, + onDidStartTerminalShellExecution((e: TerminalShellExecutionStartEvent) => { + this.onTerminalShellExecutionStartEmitter.fire(e); + }), + onDidEndTerminalShellExecution((e: TerminalShellExecutionEndEvent) => { + this.onTerminalShellExecutionEndEmitter.fire(e); + }), + this.onTerminalClosed((terminal) => { + this.activatedTerminals.delete(terminal); + this.activatingTerminals.delete(terminal); + this.deactivatingTerminals.delete(terminal); + }), + ); + } + + isActivated(terminal: Terminal, environment?: PythonEnvironment): boolean { + if (!environment) { + return this.activatedTerminals.has(terminal); + } + const env = this.activatedTerminals.get(terminal); + return env?.envId.id === environment?.envId.id; + } + + getEnvironment(terminal: Terminal): PythonEnvironment | undefined { + return this.activatedTerminals.get(terminal); + } + + async activate(terminal: Terminal, environment: PythonEnvironment): Promise { + if (isTaskTerminal(terminal)) { + traceVerbose('Cannot activate environment in a task terminal'); + return; + } + + if (this.deactivatingTerminals.has(terminal)) { + traceVerbose('Terminal is being deactivated, cannot activate.'); + return this.deactivatingTerminals.get(terminal); + } + + if (this.activatingTerminals.has(terminal)) { + traceVerbose('Terminal is being activated, skipping.'); + return this.activatingTerminals.get(terminal); + } + + const terminalEnv = this.activatedTerminals.get(terminal); + if (terminalEnv) { + if (terminalEnv.envId.id === environment.envId.id) { + traceVerbose('Terminal is already activated with the same environment'); + return; + } else { + traceInfo( + `Terminal is activated with a different environment, deactivating: ${terminalEnv.environmentPath.fsPath}`, + ); + await this.deactivate(terminal); + } + } + + try { + const promise = this.activateInternal(terminal, environment); + traceVerbose(`Activating terminal: ${environment.environmentPath.fsPath}`); + this.activatingTerminals.set(terminal, promise); + await promise; + this.activatingTerminals.delete(terminal); + this.updateActivationState(terminal, environment, true); + traceInfo(`Terminal is activated: ${environment.environmentPath.fsPath}`); + } catch (ex) { + this.activatingTerminals.delete(terminal); + traceError('Failed to activate environment:\r\n', ex); + } + } + + async deactivate(terminal: Terminal): Promise { + if (isTaskTerminal(terminal)) { + traceVerbose('Cannot deactivate environment in a task terminal'); + return; + } + + if (this.activatingTerminals.has(terminal)) { + traceVerbose('Terminal is being activated, cannot deactivate.'); + return this.activatingTerminals.get(terminal); + } + + if (this.deactivatingTerminals.has(terminal)) { + traceVerbose('Terminal is being deactivated, skipping.'); + return this.deactivatingTerminals.get(terminal); + } + + const terminalEnv = this.activatedTerminals.get(terminal); + if (terminalEnv) { + try { + const promise = this.deactivateInternal(terminal, terminalEnv); + traceVerbose(`Deactivating terminal: ${terminalEnv.environmentPath.fsPath}`); + this.deactivatingTerminals.set(terminal, promise); + await promise; + this.deactivatingTerminals.delete(terminal); + this.updateActivationState(terminal, terminalEnv, false); + traceInfo(`Terminal is deactivated: ${terminalEnv.environmentPath.fsPath}`); + } catch (ex) { + this.deactivatingTerminals.delete(terminal); + traceError('Failed to deactivate environment:\r\n', ex); + } + } else { + traceVerbose('Terminal is not activated'); + } + } + + updateActivationState(terminal: Terminal, environment: PythonEnvironment, activated: boolean): void { + if (activated) { + this.activatedTerminals.set(terminal, environment); + } else { + this.activatedTerminals.delete(terminal); + } + setImmediate(() => { + this.onDidChangeTerminalActivationStateEmitter.fire({ terminal, environment, activated }); + }); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + } + + private async activateInternal(terminal: Terminal, environment: PythonEnvironment): Promise { + if (terminal.shellIntegration) { + await this.activateUsingShellIntegration(terminal.shellIntegration, terminal, environment); + } else { + this.activateLegacy(terminal, environment); + } + } + + private async deactivateInternal(terminal: Terminal, environment: PythonEnvironment): Promise { + if (terminal.shellIntegration) { + await this.deactivateUsingShellIntegration(terminal.shellIntegration, terminal, environment); + } else { + this.deactivateLegacy(terminal, environment); + } + } + + private activateLegacy(terminal: Terminal, environment: PythonEnvironment) { + const activationCommands = getActivationCommand(terminal, environment); + if (activationCommands) { + terminal.sendText(activationCommands); + this.activatedTerminals.set(terminal, environment); + } + } + + private deactivateLegacy(terminal: Terminal, environment: PythonEnvironment) { + const deactivationCommands = getDeactivationCommand(terminal, environment); + if (deactivationCommands) { + terminal.sendText(deactivationCommands); + this.activatedTerminals.delete(terminal); + } + } + + private async activateUsingShellIntegration( + shellIntegration: TerminalShellIntegration, + terminal: Terminal, + environment: PythonEnvironment, + ): Promise { + const activationCommand = getActivationCommand(terminal, environment); + if (activationCommand) { + try { + await this.executeTerminalShellCommandInternal(shellIntegration, activationCommand); + this.activatedTerminals.set(terminal, environment); + } catch { + traceError('Failed to activate environment using shell integration'); + } + } else { + traceVerbose('No activation commands found for terminal.'); + } + } + + private async deactivateUsingShellIntegration( + shellIntegration: TerminalShellIntegration, + terminal: Terminal, + environment: PythonEnvironment, + ): Promise { + const deactivationCommand = getDeactivationCommand(terminal, environment); + if (deactivationCommand) { + try { + await this.executeTerminalShellCommandInternal(shellIntegration, deactivationCommand); + this.activatedTerminals.delete(terminal); + } catch { + traceError('Failed to deactivate environment using shell integration'); + } + } else { + traceVerbose('No deactivation commands found for terminal.'); + } + } + + private async executeTerminalShellCommandInternal( + shellIntegration: TerminalShellIntegration, + command: string, + ): Promise { + const execution = shellIntegration.executeCommand(command); + const disposables: Disposable[] = []; + + const promise = new Promise((resolve) => { + const timer = setTimeout(() => { + traceError(`Shell execution timed out: ${command}`); + resolve(); + }, 2000); + + disposables.push( + new Disposable(() => clearTimeout(timer)), + this.onTerminalShellExecutionEnd((e: TerminalShellExecutionEndEvent) => { + if (e.execution === execution) { + resolve(); + } + }), + this.onTerminalShellExecutionStart((e: TerminalShellExecutionStartEvent) => { + if (e.execution === execution) { + traceVerbose(`Shell execution started: ${command}`); + } + }), + ); + }); + + try { + await promise; + return true; + } catch { + traceError(`Failed to execute shell command: ${command}`); + return false; + } finally { + disposables.forEach((d) => d.dispose()); + } + } +} diff --git a/src/features/terminal/terminalEnvVarInjector.ts b/src/features/terminal/terminalEnvVarInjector.ts new file mode 100644 index 0000000..039c4b3 --- /dev/null +++ b/src/features/terminal/terminalEnvVarInjector.ts @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { + Disposable, + EnvironmentVariableScope, + GlobalEnvironmentVariableCollection, + window, + workspace, + WorkspaceFolder, +} from 'vscode'; +import { traceError, traceVerbose } from '../../common/logging'; +import { resolveVariables } from '../../common/utils/internalVariables'; +import { getConfiguration, getWorkspaceFolder } from '../../common/workspace.apis'; +import { EnvVarManager } from '../execution/envVariableManager'; + +/** + * Manages injection of workspace-specific environment variables into VS Code terminals + * using the GlobalEnvironmentVariableCollection API. + */ +export class TerminalEnvVarInjector implements Disposable { + private disposables: Disposable[] = []; + + constructor( + private readonly envVarCollection: GlobalEnvironmentVariableCollection, + private readonly envVarManager: EnvVarManager, + ) { + this.initialize(); + } + + /** + * Initialize the injector by setting up watchers and injecting initial environment variables. + */ + private async initialize(): Promise { + traceVerbose('TerminalEnvVarInjector: Initializing environment variable injection'); + + // Listen for environment variable changes from the manager + this.disposables.push( + this.envVarManager.onDidChangeEnvironmentVariables((args) => { + if (!args.uri) { + // No specific URI, reload all workspaces + this.updateEnvironmentVariables().catch((error) => { + traceError('Failed to update environment variables:', error); + }); + return; + } + + const affectedWorkspace = getWorkspaceFolder(args.uri); + if (!affectedWorkspace) { + // No workspace folder found for this URI, reloading all workspaces + this.updateEnvironmentVariables().catch((error) => { + traceError('Failed to update environment variables:', error); + }); + return; + } + + // Check if env file injection is enabled when variables change + const config = getConfiguration('python', args.uri); + const useEnvFile = config.get('terminal.useEnvFile', false); + const envFilePath = config.get('envFile'); + + // Only show notification when env vars change and we have an env file but injection is disabled + if (!useEnvFile && envFilePath) { + window.showInformationMessage( + 'An environment file is configured but terminal environment injection is disabled. Enable "python.terminal.useEnvFile" to use environment variables from .env files in terminals.', + ); + } + + if (args.changeType === 2) { + // FileChangeType.Deleted + this.clearWorkspaceVariables(affectedWorkspace); + } else { + this.updateEnvironmentVariables(affectedWorkspace).catch((error) => { + traceError('Failed to update environment variables:', error); + }); + } + }), + ); + + // Listen for changes to the python.envFile setting + this.disposables.push( + workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('python.envFile')) { + traceVerbose( + 'TerminalEnvVarInjector: python.envFile setting changed, updating environment variables', + ); + this.updateEnvironmentVariables().catch((error) => { + traceError('Failed to update environment variables:', error); + }); + } + }), + ); + + // Initial load of environment variables + await this.updateEnvironmentVariables(); + } + + /** + * Update environment variables in the terminal collection. + */ + private async updateEnvironmentVariables(workspaceFolder?: WorkspaceFolder): Promise { + try { + if (workspaceFolder) { + // Update only the specified workspace + traceVerbose( + `TerminalEnvVarInjector: Updating environment variables for workspace: ${workspaceFolder.uri.fsPath}`, + ); + await this.injectEnvironmentVariablesForWorkspace(workspaceFolder); + } else { + // No provided workspace - update all workspaces + + const workspaceFolders = workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + traceVerbose('TerminalEnvVarInjector: No workspace folders found, skipping env var injection'); + return; + } + + traceVerbose('TerminalEnvVarInjector: Updating environment variables for all workspaces'); + for (const folder of workspaceFolders) { + await this.injectEnvironmentVariablesForWorkspace(folder); + } + } + + traceVerbose('TerminalEnvVarInjector: Environment variable injection completed'); + } catch (error) { + traceError('TerminalEnvVarInjector: Error updating environment variables:', error); + } + } + + /** + * Inject environment variables for a specific workspace. + */ + private async injectEnvironmentVariablesForWorkspace(workspaceFolder: WorkspaceFolder): Promise { + const workspaceUri = workspaceFolder.uri; + try { + const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri); + + // use scoped environment variable collection + const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder }); + + // Check if env file injection is enabled + const config = getConfiguration('python', workspaceUri); + const useEnvFile = config.get('terminal.useEnvFile', false); + const envFilePath = config.get('envFile'); + + if (!useEnvFile) { + traceVerbose( + `TerminalEnvVarInjector: Env file injection disabled for workspace: ${workspaceUri.fsPath}`, + ); + return; + } + + // Track which .env file is being used for logging + const resolvedEnvFilePath: string | undefined = envFilePath + ? path.resolve(resolveVariables(envFilePath, workspaceUri)) + : undefined; + const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env'); + + let activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath; + if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) { + traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`); + } else { + traceVerbose( + `TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`, + ); + return; // No .env file to inject + } + + for (const [key, value] of Object.entries(envVars)) { + if (value === undefined) { + // Remove the environment variable if the value is undefined + envVarScope.delete(key); + } else { + envVarScope.replace(key, value); + } + } + } catch (error) { + traceError( + `TerminalEnvVarInjector: Error injecting environment variables for workspace ${workspaceUri.fsPath}:`, + error, + ); + } + } + + /** + * Dispose of the injector and clean up resources. + */ + dispose(): void { + traceVerbose('TerminalEnvVarInjector: Disposing'); + this.disposables.forEach((disposable) => disposable.dispose()); + this.disposables = []; + + // Clear all environment variables from the collection + this.envVarCollection.clear(); + } + + private getEnvironmentVariableCollectionScoped(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.envVarCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + + /** + * Clear all environment variables for a workspace. + */ + private clearWorkspaceVariables(workspaceFolder: WorkspaceFolder): void { + try { + const scope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder }); + scope.clear(); + } catch (error) { + traceError(`Failed to clear environment variables for workspace ${workspaceFolder.uri.fsPath}:`, error); + } + } +} diff --git a/src/features/terminal/terminalManager.ts b/src/features/terminal/terminalManager.ts new file mode 100644 index 0000000..f7acb2d --- /dev/null +++ b/src/features/terminal/terminalManager.ts @@ -0,0 +1,461 @@ +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { Disposable, EventEmitter, ProgressLocation, Terminal, TerminalOptions, Uri } from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi, PythonProject, PythonTerminalCreateOptions } from '../../api'; +import { ActivationStrings } from '../../common/localize'; +import { traceInfo, traceVerbose } from '../../common/logging'; +import { + createTerminal, + onDidChangeWindowState, + onDidCloseTerminal, + onDidOpenTerminal, + terminals, + withProgress, +} from '../../common/window.apis'; +import { getConfiguration, onDidChangeConfiguration } from '../../common/workspace.apis'; +import { isActivatableEnvironment } from '../common/activation'; +import { identifyTerminalShell } from '../common/shellDetector'; +import { getPythonApi } from '../pythonApi'; +import { getShellIntegrationEnabledCache, isWsl, shellIntegrationForActiveTerminal } from './shells/common/shellUtils'; +import { ShellEnvsProvider, ShellSetupState, ShellStartupScriptProvider } from './shells/startupProvider'; +import { handleSettingUpShellProfile } from './shellStartupSetupHandlers'; +import { + DidChangeTerminalActivationStateEvent, + TerminalActivation, + TerminalActivationInternal, + TerminalEnvironment, +} from './terminalActivationState'; +import { + ACT_TYPE_COMMAND, + ACT_TYPE_OFF, + ACT_TYPE_SHELL, + AutoActivationType, + getAutoActivationType, + getEnvironmentForTerminal, + waitForShellIntegration, +} from './utils'; + +export interface TerminalCreation { + create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +export interface TerminalGetters { + getProjectTerminal( + project: Uri | PythonProject, + environment: PythonEnvironment, + createNew?: boolean, + ): Promise; + getDedicatedTerminal( + terminalKey: Uri | string, + project: Uri | PythonProject, + environment: PythonEnvironment, + createNew?: boolean, + ): Promise; +} + +export interface TerminalInit { + initialize(api: PythonEnvironmentApi): Promise; +} + +export interface TerminalManager + extends TerminalEnvironment, + TerminalInit, + TerminalActivation, + TerminalCreation, + TerminalGetters, + Disposable {} + +export class TerminalManagerImpl implements TerminalManager { + private disposables: Disposable[] = []; + private skipActivationOnOpen = new Set(); + private shellSetup: Map = new Map(); + + private onTerminalOpenedEmitter = new EventEmitter(); + private onTerminalOpened = this.onTerminalOpenedEmitter.event; + + private onTerminalClosedEmitter = new EventEmitter(); + private onTerminalClosed = this.onTerminalClosedEmitter.event; + + private onDidChangeTerminalActivationStateEmitter = new EventEmitter(); + public onDidChangeTerminalActivationState = this.onDidChangeTerminalActivationStateEmitter.event; + + private hasFocus = true; + + constructor( + private readonly ta: TerminalActivationInternal, + private readonly startupEnvProviders: ShellEnvsProvider[], + private readonly startupScriptProviders: ShellStartupScriptProvider[], + ) { + this.disposables.push( + this.onTerminalOpenedEmitter, + this.onTerminalClosedEmitter, + this.onDidChangeTerminalActivationStateEmitter, + onDidOpenTerminal((t: Terminal) => { + this.onTerminalOpenedEmitter.fire(t); + }), + onDidCloseTerminal((t: Terminal) => { + this.onTerminalClosedEmitter.fire(t); + }), + this.onTerminalOpened(async (t) => { + if (this.skipActivationOnOpen.has(t) || (t.creationOptions as TerminalOptions)?.hideFromUser) { + return; + } + let env = this.ta.getEnvironment(t); + if (!env) { + const api = await getPythonApi(); + env = await getEnvironmentForTerminal(api, t); + } + if (env) { + await this.autoActivateOnTerminalOpen(t, env); + } + }), + this.onTerminalClosed((t) => { + this.skipActivationOnOpen.delete(t); + }), + this.ta.onDidChangeTerminalActivationState((e) => { + this.onDidChangeTerminalActivationStateEmitter.fire(e); + }), + onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('python-envs.terminal.autoActivationType')) { + const actType = getAutoActivationType(); + if (actType === ACT_TYPE_SHELL) { + traceInfo(`Auto activation type changed to ${actType}`); + const shells = new Set( + terminals() + .map((t) => identifyTerminalShell(t)) + .filter((t) => t !== 'unknown'), + ); + if (shells.size > 0) { + await this.handleSetupCheck(shells); + } + } else { + traceVerbose( + `Auto activation type changed to ${actType}, we are cleaning up shell startup setup`, + ); + // Teardown scripts when switching away from shell startup activation + await Promise.all(this.startupScriptProviders.map((p) => p.teardownScripts())); + this.shellSetup.clear(); + } + } + if (e.affectsConfiguration('terminal.integrated.shellIntegration.enabled')) { + traceInfo('Shell integration setting changed, invalidating cache'); + const updatedShellIntegrationSetting = await getShellIntegrationEnabledCache(); + if (!updatedShellIntegrationSetting) { + const shells = new Set( + terminals() + .map((t) => identifyTerminalShell(t)) + .filter((t) => t !== 'unknown'), + ); + if (shells.size > 0) { + await this.handleSetupCheck(shells); + } + } + } + }), + onDidChangeWindowState((e) => { + this.hasFocus = e.focused; + }), + ); + } + + private async handleSetupCheck(shellType: string | Set): Promise { + const shellTypes = typeof shellType === 'string' ? new Set([shellType]) : shellType; + const providers = this.startupScriptProviders.filter((p) => shellTypes.has(p.shellType)); + if (providers.length > 0) { + const shellsToSetup: ShellStartupScriptProvider[] = []; + await Promise.all( + providers.map(async (p) => { + const state = await p.isSetup(); + const shellIntegrationEnabled = await getShellIntegrationEnabledCache(); + traceVerbose(`Checking shell profile for ${p.shellType}, with state: ${state}`); + if (state === ShellSetupState.NotSetup) { + traceVerbose( + `WSL detected: ${isWsl()}, Shell integration available from setting, or active terminal: ${shellIntegrationEnabled}, or ${await shellIntegrationForActiveTerminal( + p.name, + )}`, + ); + + if ( + (shellIntegrationEnabled || (await shellIntegrationForActiveTerminal(p.name))) && + !isWsl() + ) { + // Shell integration available and NOT in WSL - skip setup + await p.teardownScripts(); + this.shellSetup.set(p.shellType, true); + traceVerbose( + `Shell integration available for ${p.shellType} (not WSL), skipping prompt, and profile modification.`, + ); + } else { + // WSL (regardless of integration) OR no shell integration - needs setup + this.shellSetup.set(p.shellType, false); + shellsToSetup.push(p); + traceVerbose( + `Shell integration is NOT available. Shell profile for ${p.shellType} is not setup.`, + ); + } + } else if (state === ShellSetupState.Setup) { + if ( + (shellIntegrationEnabled || (await shellIntegrationForActiveTerminal(p.name))) && + !isWsl() + ) { + await p.teardownScripts(); + traceVerbose( + `Shell integration available for ${p.shellType}, removed profile script in favor of shell integration.`, + ); + } + this.shellSetup.set(p.shellType, true); + traceVerbose(`Shell profile for ${p.shellType} is setup.`); + } else if (state === ShellSetupState.NotInstalled) { + this.shellSetup.set(p.shellType, false); + traceVerbose(`Shell profile for ${p.shellType} is not installed.`); + } + }), + ); + + if (shellsToSetup.length === 0) { + traceVerbose(`No shell profiles to setup for ${Array.from(shellTypes).join(', ')}`); + return; + } + + if (!this.hasFocus) { + traceVerbose('Window does not have focus, skipping shell profile setup'); + return; + } + + setImmediate(async () => { + // Avoid blocking this setup on user interaction. + await handleSettingUpShellProfile(shellsToSetup, (p, v) => this.shellSetup.set(p.shellType, v)); + }); + } + } + + private getShellActivationType(shellType: string): AutoActivationType | undefined { + let isSetup = this.shellSetup.get(shellType); + if (isSetup === true) { + traceVerbose(`Shell profile for ${shellType} is already setup.`); + return ACT_TYPE_SHELL; + } else if (isSetup === false) { + traceVerbose(`Shell profile for ${shellType} is not set up, using command fallback.`); + return ACT_TYPE_COMMAND; + } + } + + private async getEffectiveActivationType(shellType: string): Promise { + const providers = this.startupScriptProviders.filter((p) => p.shellType === shellType); + if (providers.length > 0) { + traceVerbose(`Shell startup is supported for ${shellType}, using shell startup activation`); + let isSetup = this.getShellActivationType(shellType); + if (isSetup !== undefined) { + return isSetup; + } + + await this.handleSetupCheck(shellType); + + // Check again after the setup check. + return this.getShellActivationType(shellType) ?? ACT_TYPE_COMMAND; + } + traceInfo(`Shell startup not supported for ${shellType}, using command activation as fallback`); + return ACT_TYPE_COMMAND; + } + + private async autoActivateOnTerminalOpen(terminal: Terminal, environment: PythonEnvironment): Promise { + let actType = getAutoActivationType(); + const shellType = identifyTerminalShell(terminal); + if (actType === ACT_TYPE_SHELL) { + await this.handleSetupCheck(shellType); + actType = await this.getEffectiveActivationType(shellType); + } + + if (actType === ACT_TYPE_COMMAND) { + if (isActivatableEnvironment(environment)) { + await withProgress( + { + location: ProgressLocation.Window, + title: `${ActivationStrings.activatingEnvironment}: ${environment.environmentPath.fsPath}`, + }, + async () => { + await waitForShellIntegration(terminal); + await this.activate(terminal, environment); + }, + ); + } else { + traceVerbose(`Environment ${environment.environmentPath.fsPath} is not activatable`); + } + } else if (actType === ACT_TYPE_OFF) { + traceInfo(`"python-envs.terminal.autoActivationType" is set to "${actType}", skipping auto activation`); + } else if (actType === ACT_TYPE_SHELL) { + traceInfo( + `"python-envs.terminal.autoActivationType" is set to "${actType}", terminal should be activated by shell startup script`, + ); + } + } + + public async create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise { + const autoActType = getAutoActivationType(); + let envVars = options.env; + if (autoActType === ACT_TYPE_SHELL) { + const vars = await Promise.all(this.startupEnvProviders.map((p) => p.getEnvVariables(environment))); + + vars.forEach((varMap) => { + if (varMap) { + varMap.forEach((value, key) => { + envVars = { ...envVars, [key]: value }; + }); + } + }); + } + + // Uncomment the code line below after the issue is resolved: + // https://github.com/microsoft/vscode-python-environments/issues/172 + // const name = options.name ?? `Python: ${environment.displayName}`; + const newTerminal = createTerminal({ + name: options.name, + shellPath: options.shellPath, + shellArgs: options.shellArgs, + cwd: options.cwd, + env: envVars, + strictEnv: options.strictEnv, + message: options.message, + iconPath: options.iconPath, + hideFromUser: options.hideFromUser, + color: options.color, + location: options.location, + isTransient: options.isTransient, + }); + + if (autoActType === ACT_TYPE_COMMAND) { + if (options.disableActivation) { + this.skipActivationOnOpen.add(newTerminal); + return newTerminal; + } + + // We add it to skip activation on open to prevent double activation. + // We can activate it ourselves since we are creating it. + this.skipActivationOnOpen.add(newTerminal); + await this.autoActivateOnTerminalOpen(newTerminal, environment); + } + + return newTerminal; + } + + private dedicatedTerminals = new Map(); + async getDedicatedTerminal( + terminalKey: Uri, + project: Uri | PythonProject, + environment: PythonEnvironment, + createNew: boolean = false, + ): Promise { + const part = terminalKey instanceof Uri ? path.normalize(terminalKey.fsPath) : terminalKey; + const key = `${environment.envId.id}:${part}`; + if (!createNew) { + const terminal = this.dedicatedTerminals.get(key); + if (terminal) { + return terminal; + } + } + + const puri = project instanceof Uri ? project : project.uri; + const config = getConfiguration('python', terminalKey); + const projectStat = await fsapi.stat(puri.fsPath); + const projectDir = projectStat.isDirectory() ? puri.fsPath : path.dirname(puri.fsPath); + + const uriStat = await fsapi.stat(terminalKey.fsPath); + const uriDir = uriStat.isDirectory() ? terminalKey.fsPath : path.dirname(terminalKey.fsPath); + const cwd = config.get('terminal.executeInFileDir', false) ? uriDir : projectDir; + + const newTerminal = await this.create(environment, { cwd }); + this.dedicatedTerminals.set(key, newTerminal); + + const disable = onDidCloseTerminal((terminal) => { + if (terminal === newTerminal) { + this.dedicatedTerminals.delete(key); + disable.dispose(); + } + }); + + return newTerminal; + } + + private projectTerminals = new Map(); + async getProjectTerminal( + project: Uri | PythonProject, + environment: PythonEnvironment, + createNew: boolean = false, + ): Promise { + const uri = project instanceof Uri ? project : project.uri; + const key = `${environment.envId.id}:${path.normalize(uri.fsPath)}`; + if (!createNew) { + const terminal = this.projectTerminals.get(key); + if (terminal) { + return terminal; + } + } + const stat = await fsapi.stat(uri.fsPath); + const cwd = stat.isDirectory() ? uri.fsPath : path.dirname(uri.fsPath); + const newTerminal = await this.create(environment, { cwd }); + this.projectTerminals.set(key, newTerminal); + + const disable = onDidCloseTerminal((terminal) => { + if (terminal === newTerminal) { + this.projectTerminals.delete(key); + disable.dispose(); + } + }); + + return newTerminal; + } + + private async activateUsingCommand(api: PythonEnvironmentApi, t: Terminal): Promise { + this.skipActivationOnOpen.add(t); + + const env = this.ta.getEnvironment(t) ?? (await getEnvironmentForTerminal(api, t)); + + if (env && isActivatableEnvironment(env)) { + await this.activate(t, env); + } + } + + public async initialize(api: PythonEnvironmentApi): Promise { + const actType = getAutoActivationType(); + if (actType === ACT_TYPE_COMMAND) { + await Promise.all(terminals().map(async (t) => this.activateUsingCommand(api, t))); + } else if (actType === ACT_TYPE_SHELL) { + const shells = new Set( + terminals() + .map((t) => identifyTerminalShell(t)) + .filter((t) => t !== 'unknown'), + ); + if (shells.size > 0) { + await this.handleSetupCheck(shells); + await Promise.all( + terminals().map(async (t) => { + // If the shell is not set up, we activate using command fallback. + if (this.shellSetup.get(identifyTerminalShell(t)) === false) { + await this.activateUsingCommand(api, t); + } + }), + ); + } + } + } + + public getEnvironment(terminal: Terminal): PythonEnvironment | undefined { + return this.ta.getEnvironment(terminal); + } + + public activate(terminal: Terminal, environment: PythonEnvironment): Promise { + return this.ta.activate(terminal, environment); + } + + public deactivate(terminal: Terminal): Promise { + return this.ta.deactivate(terminal); + } + + isActivated(terminal: Terminal, environment?: PythonEnvironment): boolean { + return this.ta.isActivated(terminal, environment); + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/features/terminal/utils.ts b/src/features/terminal/utils.ts new file mode 100644 index 0000000..c8f6443 --- /dev/null +++ b/src/features/terminal/utils.ts @@ -0,0 +1,173 @@ +import * as path from 'path'; +import { Terminal, TerminalOptions, Uri } from 'vscode'; +import { PythonEnvironment, PythonProject, PythonProjectEnvironmentApi, PythonProjectGetterApi } from '../../api'; +import { sleep } from '../../common/utils/asyncUtils'; +import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; + +export const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds +export const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds + +export async function waitForShellIntegration(terminal: Terminal): Promise { + let timeout = 0; + while (!terminal.shellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) { + await sleep(SHELL_INTEGRATION_POLL_INTERVAL); + timeout += SHELL_INTEGRATION_POLL_INTERVAL; + } + return terminal.shellIntegration !== undefined; +} + +export function isTaskTerminal(terminal: Terminal): boolean { + // TODO: Need API for core for this https://github.com/microsoft/vscode/issues/234440 + return terminal.name.toLowerCase().includes('task'); +} + +export function getTerminalCwd(terminal: Terminal): string | undefined { + if (terminal.shellIntegration?.cwd) { + return terminal.shellIntegration.cwd.fsPath; + } + const cwd = (terminal.creationOptions as TerminalOptions)?.cwd; + if (cwd) { + return typeof cwd === 'string' ? cwd : cwd.fsPath; + } + return undefined; +} + +async function getDistinctProjectEnvs( + api: PythonProjectEnvironmentApi, + projects: readonly PythonProject[], +): Promise { + const envs: PythonEnvironment[] = []; + await Promise.all( + projects.map(async (p) => { + const e = await api.getEnvironment(p.uri); + if (e && !envs.find((x) => x.envId.id === e.envId.id)) { + envs.push(e); + } + }), + ); + return envs; +} + +export async function getEnvironmentForTerminal( + api: PythonProjectGetterApi & PythonProjectEnvironmentApi, + terminal?: Terminal, +): Promise { + let env: PythonEnvironment | undefined; + + const projects = api.getPythonProjects(); + if (projects.length === 0) { + env = await api.getEnvironment(undefined); + } else if (projects.length === 1) { + env = await api.getEnvironment(projects[0].uri); + } else { + const envs = await getDistinctProjectEnvs(api, projects); + if (envs.length === 1) { + // If we have only one distinct environment, then use that. + env = envs[0]; + } else { + // If we have multiple distinct environments, then we can't pick one + // So skip selecting so we can try heuristic approach + env = undefined; + } + } + + if (env) { + return env; + } + + // This is a heuristic approach to attempt to find the environment for this terminal. + // This is not guaranteed to work, but is better than nothing. + const terminalCwd = terminal ? getTerminalCwd(terminal) : undefined; + if (terminalCwd) { + env = await api.getEnvironment(Uri.file(path.resolve(terminalCwd))); + } else { + const workspaces = getWorkspaceFolders() ?? []; + if (workspaces.length === 1) { + env = await api.getEnvironment(workspaces[0].uri); + } + } + + return env; +} + +export const ACT_TYPE_SHELL = 'shellStartup'; +export const ACT_TYPE_COMMAND = 'command'; +export const ACT_TYPE_OFF = 'off'; +export type AutoActivationType = 'off' | 'command' | 'shellStartup'; +/** + * Determines the auto-activation type for Python environments in terminals. + * + * The following types are supported: + * - 'shellStartup': Environment is activated via shell startup scripts + * - 'command': Environment is activated via explicit command + * - 'off': Auto-activation is disabled + * + * Priority order: + * 1. python-envs.terminal.autoActivationType + * a. globalRemoteValue + * b. globalLocalValue + * c. globalValue + * 2. python.terminal.activateEnvironment setting (if false, returns 'off' & sets autoActivationType to 'off') + * 3. Default to 'command' if no setting is found + * + * @returns {AutoActivationType} The determined auto-activation type + */ +export function getAutoActivationType(): AutoActivationType { + const pyEnvsConfig = getConfiguration('python-envs'); + const pyEnvsActivationType = pyEnvsConfig.inspect('terminal.autoActivationType'); + + if (pyEnvsActivationType) { + // Priority order: globalRemoteValue > globalLocalValue > globalValue + const activationType = pyEnvsActivationType as Record; + + if ('globalRemoteValue' in pyEnvsActivationType && activationType.globalRemoteValue !== undefined) { + return activationType.globalRemoteValue as AutoActivationType; + } + if ('globalLocalValue' in pyEnvsActivationType && activationType.globalLocalValue !== undefined) { + return activationType.globalLocalValue as AutoActivationType; + } + if (pyEnvsActivationType.globalValue !== undefined) { + return pyEnvsActivationType.globalValue; + } + } + + // If none of the python-envs settings are defined, check the legacy python setting + const pythonConfig = getConfiguration('python'); + const pythonActivateSetting = pythonConfig.get('terminal.activateEnvironment', undefined); + if (pythonActivateSetting === false) { + // Set autoActivationType to 'off' if python.terminal.activateEnvironment is false + pyEnvsConfig.update('terminal.autoActivationType', ACT_TYPE_OFF); + return ACT_TYPE_OFF; + } + + // Default to 'command' if no settings are found or if pythonActivateSetting is true/undefined + return ACT_TYPE_COMMAND; +} + +export async function setAutoActivationType(value: AutoActivationType): Promise { + const config = getConfiguration('python-envs'); + return await config.update('terminal.autoActivationType', value, true); +} + +export async function getAllDistinctProjectEnvironments( + api: PythonProjectGetterApi & PythonProjectEnvironmentApi, +): Promise { + const envs: PythonEnvironment[] | undefined = []; + + const projects = api.getPythonProjects(); + if (projects.length === 0) { + const env = await api.getEnvironment(undefined); + if (env) { + envs.push(env); + } + } else if (projects.length === 1) { + const env = await api.getEnvironment(projects[0].uri); + if (env) { + envs.push(env); + } + } else { + envs.push(...(await getDistinctProjectEnvs(api, projects))); + } + + return envs.length > 0 ? envs : undefined; +} diff --git a/src/features/views/envManagersView.ts b/src/features/views/envManagersView.ts index 90112b5..c418fed 100644 --- a/src/features/views/envManagersView.ts +++ b/src/features/views/envManagersView.ts @@ -1,5 +1,5 @@ import { Disposable, Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem, TreeView, window } from 'vscode'; -import { PythonEnvironment } from '../../api'; +import { DidChangeEnvironmentEventArgs, EnvironmentGroupInfo, PythonEnvironment } from '../../api'; import { DidChangeEnvironmentManagerEventArgs, DidChangePackageManagerEventArgs, @@ -9,28 +9,27 @@ import { InternalEnvironmentManager, InternalPackageManager, } from '../../internal.api'; -import { traceError, traceVerbose } from '../../common/logging'; import { EnvTreeItem, EnvManagerTreeItem, PythonEnvTreeItem, - PackageRootTreeItem, PackageTreeItem, EnvTreeItemKind, NoPythonEnvTreeItem, EnvInfoTreeItem, - PackageRootInfoTreeItem, + PythonGroupEnvTreeItem, } from './treeViewItems'; +import { createSimpleDebounce } from '../../common/utils/debounce'; +import { ProjectViews } from '../../common/localize'; export class EnvManagerView implements TreeDataProvider, Disposable { private treeView: TreeView; - private _treeDataChanged: EventEmitter = new EventEmitter< + private treeDataChanged: EventEmitter = new EventEmitter< EnvTreeItem | EnvTreeItem[] | null | undefined >(); - private _viewsManagers = new Map(); - private _viewsEnvironments = new Map(); - private _viewsPackageRoots = new Map(); - private _viewsPackages = new Map(); + private revealMap = new Map(); + private managerViews = new Map(); + private selected: Map = new Map(); private disposables: Disposable[] = []; public constructor(public providers: EnvironmentManagers) { @@ -39,8 +38,13 @@ export class EnvManagerView implements TreeDataProvider, Disposable }); this.disposables.push( + new Disposable(() => { + this.revealMap.clear(); + this.managerViews.clear(); + this.selected.clear(); + }), this.treeView, - this._treeDataChanged, + this.treeDataChanged, this.providers.onDidChangeEnvironments((e: InternalDidChangeEnvironmentsEventArgs) => { this.onDidChangeEnvironments(e); }), @@ -58,23 +62,19 @@ export class EnvManagerView implements TreeDataProvider, Disposable } dispose() { - this._viewsManagers.clear(); - this._viewsEnvironments.clear(); - this._viewsPackages.clear(); this.disposables.forEach((d) => d.dispose()); } + private debouncedFireDataChanged = createSimpleDebounce(500, () => this.treeDataChanged.fire(undefined)); private fireDataChanged(item: EnvTreeItem | EnvTreeItem[] | null | undefined) { - if (Array.isArray(item)) { - if (item.length > 0) { - this._treeDataChanged.fire(item); - } + if (item) { + this.treeDataChanged.fire(item); } else { - this._treeDataChanged.fire(item); + this.debouncedFireDataChanged.trigger(); } } - onDidChangeTreeData: Event = this._treeDataChanged.event; + onDidChangeTreeData: Event = this.treeDataChanged.event; getTreeItem(element: EnvTreeItem): TreeItem | Thenable { return element.treeItem; @@ -82,55 +82,93 @@ export class EnvManagerView implements TreeDataProvider, Disposable async getChildren(element?: EnvTreeItem | undefined): Promise { if (!element) { - return Array.from(this._viewsManagers.values()); + const views: EnvTreeItem[] = []; + this.managerViews.clear(); + this.providers.managers.forEach((m) => { + const view = new EnvManagerTreeItem(m); + views.push(view); + this.managerViews.set(m.id, view); + }); + return views; } + if (element.kind === EnvTreeItemKind.manager) { const manager = (element as EnvManagerTreeItem).manager; const views: EnvTreeItem[] = []; const envs = await manager.getEnvironments('all'); - envs.forEach((env) => { - const view = this._viewsEnvironments.get(env.envId.id); - if (view) { - views.push(view); + envs.filter((e) => !e.group).forEach((env) => { + const view = new PythonEnvTreeItem(env, element as EnvManagerTreeItem, this.selected.get(env.envId.id)); + views.push(view); + this.revealMap.set(env.envId.id, view); + }); + + const groups: string[] = []; + const groupObjects: (string | EnvironmentGroupInfo)[] = []; + envs.filter((e) => e.group).forEach((env) => { + const name = + env.group && typeof env.group === 'string' ? env.group : (env.group as EnvironmentGroupInfo).name; + if (name && !groups.includes(name)) { + groups.push(name); + groupObjects.push(env.group as EnvironmentGroupInfo); } }); + groupObjects.forEach((group) => { + views.push(new PythonGroupEnvTreeItem(element as EnvManagerTreeItem, group)); + }); + if (views.length === 0) { views.push(new NoPythonEnvTreeItem(element as EnvManagerTreeItem)); } return views; } - if (element.kind === EnvTreeItemKind.environment) { - const environment = (element as PythonEnvTreeItem).environment; - const envManager = (element as PythonEnvTreeItem).parent.manager; - const pkgManager = this.getSupportedPackageManager(envManager); - const parent = element as PythonEnvTreeItem; + if (element.kind === EnvTreeItemKind.environmentGroup) { + const groupItem = element as PythonGroupEnvTreeItem; + const manager = groupItem.parent.manager; const views: EnvTreeItem[] = []; + const envs = await manager.getEnvironments('all'); + const groupName = + typeof groupItem.group === 'string' ? groupItem.group : (groupItem.group as EnvironmentGroupInfo).name; + const grouped = envs.filter((e) => { + if (e.group) { + const name = + e.group && typeof e.group === 'string' ? e.group : (e.group as EnvironmentGroupInfo).name; + return name === groupName; + } + return false; + }); - if (pkgManager) { - const item = new PackageRootTreeItem(parent, pkgManager, environment); - this._viewsPackageRoots.set(environment.envId.id, item); - views.push(item); - } else { - views.push(new EnvInfoTreeItem(parent, 'No package manager found')); - } + grouped.forEach((env) => { + const view = new PythonEnvTreeItem(env, groupItem, this.selected.get(env.envId.id)); + views.push(view); + this.revealMap.set(env.envId.id, view); + }); return views; } - if (element.kind === EnvTreeItemKind.packageRoot) { - const root = element as PackageRootTreeItem; - const manager = root.manager; - const environment = root.environment; + if (element.kind === EnvTreeItemKind.environment) { + const pythonEnvItem = element as PythonEnvTreeItem; + const environment = pythonEnvItem.environment; + const envManager = + pythonEnvItem.parent.kind === EnvTreeItemKind.environmentGroup + ? pythonEnvItem.parent.parent.manager + : pythonEnvItem.parent.manager; - let packages = await manager.getPackages(environment); + const pkgManager = this.getSupportedPackageManager(envManager); + const parent = element as PythonEnvTreeItem; const views: EnvTreeItem[] = []; - if (packages) { - views.push(...packages.map((p) => new PackageTreeItem(p, root, manager))); + if (pkgManager) { + const packages = await pkgManager.getPackages(environment); + if (packages && packages.length > 0) { + views.push(...packages.map((p) => new PackageTreeItem(p, parent, pkgManager))); + } else { + views.push(new EnvInfoTreeItem(parent, ProjectViews.noPackages)); + } } else { - views.push(new PackageRootInfoTreeItem(root, 'No packages found')); + views.push(new EnvInfoTreeItem(parent, ProjectViews.noPackageManager)); } return views; @@ -141,12 +179,12 @@ export class EnvManagerView implements TreeDataProvider, Disposable return element.parent; } - async reveal(environment?: PythonEnvironment) { - if (environment && this.treeView.visible) { - const view = this._viewsEnvironments.get(environment.envId.id); - if (view) { + reveal(environment?: PythonEnvironment) { + const view = environment ? this.revealMap.get(environment.envId.id) : undefined; + if (view && this.treeView.visible) { + setImmediate(async () => { await this.treeView.reveal(view); - } + }); } } @@ -154,64 +192,45 @@ export class EnvManagerView implements TreeDataProvider, Disposable return this.providers.getPackageManager(manager.preferredPackageManagerId); } - private onDidChangeEnvironmentManager(args: DidChangeEnvironmentManagerEventArgs) { - if (args.kind === 'registered') { - this._viewsManagers.set(args.manager.id, new EnvManagerTreeItem(args.manager)); - this.fireDataChanged(undefined); - } else { - if (this._viewsManagers.delete(args.manager.id)) { - this.fireDataChanged(undefined); - } - } + private onDidChangeEnvironmentManager(_args: DidChangeEnvironmentManagerEventArgs) { + this.fireDataChanged(undefined); } private onDidChangeEnvironments(args: InternalDidChangeEnvironmentsEventArgs) { - const managerView = this._viewsManagers.get(args.manager.id); - if (!managerView) { - traceError(`No manager found: ${args.manager.id}`); - traceError(`Managers: ${this.providers.managers.map((m) => m.id).join(', ')}`); - return; - } - - // All removes should happen first, then adds - const sorted = args.changes.sort((a, b) => { - if (a.kind === 'remove' && b.kind === 'add') { - return -1; - } - if (a.kind === 'add' && b.kind === 'remove') { - return 1; - } - return 0; - }); - - sorted.forEach((e) => { - if (managerView) { - if (e.kind === 'add') { - this._viewsEnvironments.set( - e.environment.envId.id, - new PythonEnvTreeItem(e.environment, managerView), - ); - } else if (e.kind === 'remove') { - this._viewsEnvironments.delete(e.environment.envId.id); - } - } - }); - this.fireDataChanged([managerView]); + this.fireDataChanged(this.managerViews.get(args.manager.id)); } private onDidChangePackages(args: InternalDidChangePackagesEventArgs) { - const pkgRoot = this._viewsPackageRoots.get(args.environment.envId.id); - if (!pkgRoot) { - traceVerbose(`Add Package Error: No environment view found for: ${args.environment.envId.id}`); - traceVerbose(`Environment views: ${Array.from(this._viewsEnvironments.keys()).join(', ')}`); - return; + const view = Array.from(this.revealMap.values()).find( + (v) => v.environment.envId.id === args.environment.envId.id + ); + if (view) { + this.fireDataChanged(view); } + } - this.fireDataChanged(pkgRoot); + private onDidChangePackageManager(_args: DidChangePackageManagerEventArgs) { + // Since we removed the packageRoots level, just refresh all environments + // This is a simplified approach that isn't as targeted but ensures packages get refreshed + this.fireDataChanged(undefined); } - private onDidChangePackageManager(args: DidChangePackageManagerEventArgs) { - const roots = Array.from(this._viewsPackageRoots.values()).filter((r) => r.manager.id === args.manager.id); - this.fireDataChanged(roots); + public environmentChanged(e: DidChangeEnvironmentEventArgs) { + const views = []; + if (e.old) { + this.selected.delete(e.old.envId.id); + const view: EnvTreeItem | undefined = this.revealMap.get(e.old.envId.id); + if (view) { + views.push(view); + } + } + if (e.new) { + this.selected.set(e.new.envId.id, e.uri === undefined ? 'global' : e.uri.fsPath); + const view: EnvTreeItem | undefined = this.revealMap.get(e.new.envId.id); + if (view && !views.includes(view)) { + views.push(view); + } + } + this.fireDataChanged(views); } } diff --git a/src/features/views/projectView.ts b/src/features/views/projectView.ts index 317ee71..2c405a0 100644 --- a/src/features/views/projectView.ts +++ b/src/features/views/projectView.ts @@ -9,48 +9,65 @@ import { Uri, window, } from 'vscode'; -import { PythonProject, PythonEnvironment } from '../../api'; +import { PythonEnvironment } from '../../api'; +import { ProjectViews } from '../../common/localize'; +import { createSimpleDebounce } from '../../common/utils/debounce'; +import { onDidChangeConfiguration } from '../../common/workspace.apis'; import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; import { - ProjectTreeItem, - ProjectItem, - ProjectEnvironment, - ProjectPackageRootTreeItem, - ProjectTreeItemKind, + GlobalProjectItem, NoProjectEnvironment, + ProjectEnvironment, ProjectEnvironmentInfo, + ProjectItem, ProjectPackage, - ProjectPackageRootInfoTreeItem, + ProjectTreeItem, + ProjectTreeItemKind, } from './treeViewItems'; -export class WorkspaceView implements TreeDataProvider { +export class ProjectView implements TreeDataProvider { private treeView: TreeView; private _treeDataChanged: EventEmitter = new EventEmitter< ProjectTreeItem | ProjectTreeItem[] | null | undefined >(); - private _projectViews: Map = new Map(); - private _environmentViews: Map = new Map(); - private _viewsPackageRoots: Map = new Map(); + private projectViews: Map = new Map(); + private revealMap: Map = new Map(); + private packageRoots: Map = new Map(); private disposables: Disposable[] = []; + private debouncedUpdateProject = createSimpleDebounce(500, () => this.updateProject()); public constructor(private envManagers: EnvironmentManagers, private projectManager: PythonProjectManager) { this.treeView = window.createTreeView('python-projects', { treeDataProvider: this, }); this.disposables.push( + new Disposable(() => { + this.packageRoots.clear(); + this.revealMap.clear(); + this.projectViews.clear(); + }), this.treeView, this._treeDataChanged, this.projectManager.onDidChangeProjects(() => { - this.updateProject(); + this.debouncedUpdateProject.trigger(); }), - this.envManagers.onDidChangeEnvironment((e) => { - this.updateProject(this.projectManager.get(e.uri)); + this.envManagers.onDidChangeEnvironment(() => { + this.debouncedUpdateProject.trigger(); }), this.envManagers.onDidChangeEnvironments(() => { - this.updateProject(); + this.debouncedUpdateProject.trigger(); }), this.envManagers.onDidChangePackages((e) => { this.updatePackagesForEnvironment(e.environment); }), + onDidChangeConfiguration(async (e) => { + if ( + e.affectsConfiguration('python-envs.defaultEnvManager') || + e.affectsConfiguration('python-envs.pythonProjects') || + e.affectsConfiguration('python-envs.defaultPackageManager') + ) { + this.debouncedUpdateProject.trigger(); + } + }), ); } @@ -58,33 +75,14 @@ export class WorkspaceView implements TreeDataProvider { this.projectManager.initialize(); } - updateProject(p?: PythonProject | PythonProject[]): void { - if (Array.isArray(p)) { - const views: ProjectItem[] = []; - p.forEach((w) => { - const view = this._projectViews.get(ProjectItem.getId(w)); - if (view) { - this._environmentViews.delete(view.id); - views.push(view); - } - }); - this._treeDataChanged.fire(views); - } else if (p) { - const view = this._projectViews.get(ProjectItem.getId(p)); - if (view) { - this._environmentViews.delete(view.id); - this._treeDataChanged.fire(view); - } - } else { - this._projectViews.clear(); - this._environmentViews.clear(); - this._treeDataChanged.fire(undefined); - } + updateProject(): void { + this._treeDataChanged.fire(undefined); } private updatePackagesForEnvironment(e: PythonEnvironment): void { const views: ProjectTreeItem[] = []; - this._viewsPackageRoots.forEach((v) => { + // Look for environments matching this environment ID and refresh them + this.revealMap.forEach((v) => { if (v.environment.envId.id === e.envId.id) { views.push(v); } @@ -92,18 +90,30 @@ export class WorkspaceView implements TreeDataProvider { this._treeDataChanged.fire(views); } - async reveal(uri: Uri): Promise { + private revealInternal(view: ProjectEnvironment): void { if (this.treeView.visible) { - const pw = this.projectManager.get(uri); - if (pw) { - const view = this._environmentViews.get(ProjectItem.getId(pw)); - if (view) { - await this.treeView.reveal(view); - } - return view?.environment; - } + setImmediate(async () => { + await this.treeView.reveal(view); + }); } + } + reveal(context: Uri | PythonEnvironment): PythonEnvironment | undefined { + if (context instanceof Uri) { + const pw = this.projectManager.get(context); + const key = pw ? pw.uri.fsPath : 'global'; + const view = this.revealMap.get(key); + if (view) { + this.revealInternal(view); + return view.environment; + } + } else { + const view = Array.from(this.revealMap.values()).find((v) => v.environment.envId.id === context.envId.id); + if (view) { + this.revealInternal(view); + return view.environment; + } + } return undefined; } @@ -114,73 +124,100 @@ export class WorkspaceView implements TreeDataProvider { return element.treeItem; } + /** + * Returns the children of a given element in the project tree view: + * If param is undefined, return root project items + * If param is a project, returns its environments. + * If param is an environment, returns its packages. + * @param element The tree item for which to get children. + */ async getChildren(element?: ProjectTreeItem | undefined): Promise { if (element === undefined) { + // Return the root items + this.projectViews.clear(); const views: ProjectTreeItem[] = []; - this.projectManager.getProjects().forEach((w) => { - const id = ProjectItem.getId(w); - const view = this._projectViews.get(id) ?? new ProjectItem(w); - this._projectViews.set(ProjectItem.getId(w), view); + const projects = this.projectManager.getProjects(); + projects.forEach((w) => { + const view = new ProjectItem(w); + this.projectViews.set(w.uri.fsPath, view); views.push(view); }); + if (projects.length === 0) { + views.push(new GlobalProjectItem()); + } + return views; } if (element.kind === ProjectTreeItemKind.project) { const projectItem = element as ProjectItem; - const envView = this._environmentViews.get(projectItem.id); - - const manager = this.envManagers.getEnvironmentManager(projectItem.project.uri); - const environment = await manager?.get(projectItem.project.uri); - if (!manager || !environment) { - this._environmentViews.delete(projectItem.id); - return [new NoProjectEnvironment(projectItem.project, projectItem)]; + if (this.envManagers.managers.length === 0) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + ProjectViews.waitingForEnvManager, + undefined, + undefined, + '$(loading~spin)', + ), + ]; } - const envItemId = ProjectEnvironment.getId(projectItem, environment); - if (envView && envView.id === envItemId) { - return [envView]; + const uri = projectItem.id === 'global' ? undefined : projectItem.project.uri; + const manager = this.envManagers.getEnvironmentManager(uri); + if (!manager) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + ProjectViews.noEnvironmentManager, + ProjectViews.noEnvironmentManagerDescription, + ), + ]; } - this._environmentViews.delete(projectItem.id); + const environment = await this.envManagers.getEnvironment(uri); + if (!environment) { + return [ + new NoProjectEnvironment( + projectItem.project, + projectItem, + `${ProjectViews.noEnvironmentProvided} ${manager.displayName}`, + ), + ]; + } const view = new ProjectEnvironment(projectItem, environment); - this._environmentViews.set(projectItem.id, view); + this.revealMap.set(uri ? uri.fsPath : 'global', view); return [view]; } if (element.kind === ProjectTreeItemKind.environment) { + // Return packages directly under the environment + const environmentItem = element as ProjectEnvironment; const parent = environmentItem.parent; - const pkgManager = this.envManagers.getPackageManager(parent.project.uri); + const uri = parent.id === 'global' ? undefined : parent.project.uri; + const pkgManager = this.envManagers.getPackageManager(uri); const environment = environmentItem.environment; - const views: ProjectTreeItem[] = []; + if (!pkgManager) { + return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackageManager)]; + } - if (pkgManager) { - const item = new ProjectPackageRootTreeItem(environmentItem, pkgManager, environment); - this._viewsPackageRoots.set(environment.envId.id, item); - views.push(item); - } else { - views.push(new ProjectEnvironmentInfo(environmentItem, 'No package manager found')); + let packages = await pkgManager.getPackages(environment); + if (!packages) { + return [new ProjectEnvironmentInfo(environmentItem, ProjectViews.noPackages)]; } - return views; - } - if (element.kind === ProjectTreeItemKind.packageRoot) { - const root = element as ProjectPackageRootTreeItem; - const manager = root.manager; - const environment = root.environment; - let packages = await manager.getPackages(environment); - const views: ProjectTreeItem[] = []; + // Store the reference for refreshing packages + this.packageRoots.set(uri ? uri.fsPath : 'global', environmentItem); - if (packages) { - return packages.map((p) => new ProjectPackage(root, p, manager)); - } else { - views.push(new ProjectPackageRootInfoTreeItem(root, 'No packages found')); - } + return packages.map((p) => new ProjectPackage(environmentItem, p, pkgManager)); } + //return nothing if the element is not a project, environment, or undefined return undefined; } getParent(element: ProjectTreeItem): ProviderResult { diff --git a/src/features/views/pythonStatusBar.ts b/src/features/views/pythonStatusBar.ts new file mode 100644 index 0000000..765cd23 --- /dev/null +++ b/src/features/views/pythonStatusBar.ts @@ -0,0 +1,42 @@ +import { Disposable, StatusBarAlignment, StatusBarItem, ThemeColor } from 'vscode'; +import { PythonEnvironment } from '../../api'; +import { createStatusBarItem } from '../../common/window.apis'; + +export interface PythonStatusBar extends Disposable { + show(env?: PythonEnvironment): void; + hide(): void; +} + +export class PythonStatusBarImpl implements Disposable { + private disposables: Disposable[] = []; + private readonly statusBarItem: StatusBarItem; + constructor() { + this.statusBarItem = createStatusBarItem('python.interpreterDisplay', StatusBarAlignment.Right, 100); + this.statusBarItem.command = 'python-envs.set'; + this.statusBarItem.name = 'Python Interpreter'; + this.statusBarItem.tooltip = 'Select Python Interpreter'; + this.statusBarItem.text = '$(loading~spin)'; + this.statusBarItem.show(); + this.disposables.push(this.statusBarItem); + } + + public show(env?: PythonEnvironment) { + if (env) { + this.statusBarItem.text = env.displayName ?? 'Select Python Interpreter'; + this.statusBarItem.tooltip = env.environmentPath?.fsPath ?? ''; + } else { + this.statusBarItem.text = 'Select Python Interpreter'; + this.statusBarItem.tooltip = 'Select Python Interpreter'; + } + this.statusBarItem.backgroundColor = env ? undefined : new ThemeColor('statusBarItem.warningBackground'); + this.statusBarItem.show(); + } + + public hide() { + this.statusBarItem.hide(); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/features/views/revealHandler.ts b/src/features/views/revealHandler.ts new file mode 100644 index 0000000..1f7a96e --- /dev/null +++ b/src/features/views/revealHandler.ts @@ -0,0 +1,37 @@ +import { PythonEnvironmentApi } from '../../api'; +import { isPythonProjectFile } from '../../common/utils/fileNameUtils'; +import { activeTextEditor } from '../../common/window.apis'; +import { EnvManagerView } from './envManagersView'; +import { ProjectView } from './projectView'; +import { PythonStatusBar } from './pythonStatusBar'; + +export function updateViewsAndStatus( + statusBar: PythonStatusBar, + workspaceView: ProjectView, + managerView: EnvManagerView, + api: PythonEnvironmentApi, +) { + workspaceView.updateProject(); + + const activeDocument = activeTextEditor()?.document; + if (!activeDocument || activeDocument.isUntitled || activeDocument.uri.scheme !== 'file') { + statusBar.hide(); + return; + } + + if ( + activeDocument.languageId !== 'python' && + activeDocument.languageId !== 'pip-requirements' && + !isPythonProjectFile(activeDocument.uri.fsPath) + ) { + statusBar.hide(); + return; + } + + workspaceView.reveal(activeDocument.uri); + setImmediate(async () => { + const env = await api.getEnvironment(activeDocument.uri); + statusBar.show(env); + managerView.reveal(env); + }); +} diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index ebffacc..ca9b2db 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -1,14 +1,16 @@ -import { TreeItem, TreeItemCollapsibleState, MarkdownString, Command, ThemeIcon } from 'vscode'; +import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; +import { EnvironmentGroupInfo, IconPath, Package, PythonEnvironment, PythonProject } from '../../api'; +import { EnvViewStrings } from '../../common/localize'; import { InternalEnvironmentManager, InternalPackageManager } from '../../internal.api'; -import { PythonEnvironment, IconPath, Package, PythonProject } from '../../api'; +import { isActivatableEnvironment } from '../common/activation'; +import { removable } from './utils'; export enum EnvTreeItemKind { manager = 'python-env-manager', environment = 'python-env', + environmentGroup = 'python-env-group', noEnvironment = 'python-no-env', package = 'python-package', - packageRoot = 'python-package-root', - packageRootInfo = 'python-package-root-info', managerInfo = 'python-env-manager-info', environmentInfo = 'python-env-info', packageInfo = 'python-package-info', @@ -26,34 +28,71 @@ export class EnvManagerTreeItem implements EnvTreeItem { public readonly parent: undefined; constructor(public readonly manager: InternalEnvironmentManager) { const item = new TreeItem(manager.displayName, TreeItemCollapsibleState.Collapsed); - item.iconPath = manager.iconPath; item.contextValue = this.getContextValue(); item.description = manager.description; - item.tooltip = manager.tooltip; + item.tooltip = manager.tooltip ?? manager.description; + item.iconPath = manager.iconPath; this.treeItem = item; } private getContextValue() { - const create = this.manager.supportsCreate ? '-create' : ''; - return `pythonEnvManager${create}`; + const create = this.manager.supportsCreate ? 'create' : ''; + const parts = ['pythonEnvManager', create, this.manager.id].filter(Boolean); + return parts.join(';') + ';'; + } +} + +export class PythonGroupEnvTreeItem implements EnvTreeItem { + public readonly kind = EnvTreeItemKind.environmentGroup; + public readonly treeItem: TreeItem; + constructor(public readonly parent: EnvManagerTreeItem, public readonly group: string | EnvironmentGroupInfo) { + const label = typeof group === 'string' ? group : group.name; + const item = new TreeItem(label, TreeItemCollapsibleState.Collapsed); + item.contextValue = `pythonEnvGroup;${this.parent.manager.id}:${label};`; + this.treeItem = item; + + if (typeof group !== 'string') { + item.description = group.description; + item.tooltip = group.tooltip; + item.iconPath = group.iconPath; + } } } export class PythonEnvTreeItem implements EnvTreeItem { public readonly kind = EnvTreeItemKind.environment; public readonly treeItem: TreeItem; - constructor(public readonly environment: PythonEnvironment, public readonly parent: EnvManagerTreeItem) { - const item = new TreeItem(environment.displayName ?? environment.name, TreeItemCollapsibleState.Collapsed); - item.iconPath = environment.iconPath; + constructor( + public readonly environment: PythonEnvironment, + public readonly parent: EnvManagerTreeItem | PythonGroupEnvTreeItem, + public readonly selected?: string, + ) { + let name = environment.displayName ?? environment.name; + let tooltip = environment.tooltip ?? environment.description; + if (selected) { + const selectedTooltip = + selected === 'global' ? EnvViewStrings.selectedGlobalTooltip : EnvViewStrings.selectedWorkspaceTooltip; + tooltip = tooltip ? `${selectedTooltip} ● ${tooltip}` : tooltip; + } + + const item = new TreeItem(name, TreeItemCollapsibleState.Collapsed); item.contextValue = this.getContextValue(); item.description = environment.description; - item.tooltip = environment.tooltip; + item.tooltip = tooltip; + item.iconPath = environment.iconPath; this.treeItem = item; } private getContextValue() { - const remove = this.parent.manager.supportsRemove ? '-remove' : ''; - return `pythonEnvironment${remove}`; + const activatable = isActivatableEnvironment(this.environment) ? 'activatable' : ''; + let remove = ''; + if (this.parent.kind === EnvTreeItemKind.environmentGroup) { + remove = this.parent.parent.manager.supportsRemove ? 'remove' : ''; + } else if (this.parent.kind === EnvTreeItemKind.manager) { + remove = this.parent.manager.supportsRemove ? 'remove' : ''; + } + const parts = ['pythonEnvironment', remove, activatable].filter(Boolean); + return parts.join(';') + ';'; } } @@ -87,28 +126,12 @@ export class NoPythonEnvTreeItem implements EnvTreeItem { } } -export class PackageRootTreeItem implements EnvTreeItem { - public readonly kind = EnvTreeItemKind.packageRoot; - public readonly treeItem: TreeItem; - constructor( - public readonly parent: PythonEnvTreeItem, - public readonly manager: InternalPackageManager, - public readonly environment: PythonEnvironment, - ) { - const item = new TreeItem('Packages', TreeItemCollapsibleState.Collapsed); - item.contextValue = 'python-package-root'; - item.description = manager.displayName; - item.tooltip = 'Packages installed in this environment'; - this.treeItem = item; - } -} - export class PackageTreeItem implements EnvTreeItem { public readonly kind = EnvTreeItemKind.package; public readonly treeItem: TreeItem; constructor( public readonly pkg: Package, - public readonly parent: PackageRootTreeItem, + public readonly parent: PythonEnvTreeItem, public readonly manager: InternalPackageManager, ) { const item = new TreeItem(pkg.displayName); @@ -142,10 +165,10 @@ export class EnvInfoTreeItem implements EnvTreeItem { } export class PackageRootInfoTreeItem implements EnvTreeItem { - public readonly kind = EnvTreeItemKind.packageRootInfo; + public readonly kind = EnvTreeItemKind.packageInfo; public readonly treeItem: TreeItem; constructor( - public readonly parent: PackageRootTreeItem, + public readonly parent: PythonEnvTreeItem, name: string, description?: string, tooltip?: string | MarkdownString, @@ -168,7 +191,6 @@ export enum ProjectTreeItemKind { none = 'project-no-environment', environmentInfo = 'environment-info', package = 'project-package', - packageRoot = 'project-package-root', packageRootInfo = 'project-package-root-info', } @@ -187,10 +209,10 @@ export class ProjectItem implements ProjectTreeItem { constructor(public readonly project: PythonProject) { this.id = ProjectItem.getId(this.project); const item = new TreeItem(this.project.name, TreeItemCollapsibleState.Expanded); - item.contextValue = 'python-workspace'; + item.contextValue = removable(this.project) ? 'python-workspace-removable' : 'python-workspace'; item.description = this.project.description; item.tooltip = this.project.tooltip; - item.iconPath = this.project.iconPath; + item.resourceUri = project.uri.fsPath.endsWith('.py') ? this.project.uri : undefined; this.treeItem = item; } @@ -199,12 +221,27 @@ export class ProjectItem implements ProjectTreeItem { } } +export class GlobalProjectItem implements ProjectTreeItem { + public readonly kind = ProjectTreeItemKind.project; + public readonly parent: undefined; + public readonly id: string; + public readonly treeItem: TreeItem; + constructor() { + this.id = 'global'; + const item = new TreeItem('Global', TreeItemCollapsibleState.Expanded); + item.contextValue = 'python-workspace'; + item.description = 'Global Python environment'; + item.tooltip = 'Global Python environment'; + this.treeItem = item; + } +} + export class ProjectEnvironment implements ProjectTreeItem { public readonly kind = ProjectTreeItemKind.environment; public readonly id: string; public readonly treeItem: TreeItem; constructor(public readonly parent: ProjectItem, public readonly environment: PythonEnvironment) { - this.id = ProjectEnvironment.getId(parent, environment); + this.id = this.getId(parent, environment); const item = new TreeItem( this.environment.displayName ?? this.environment.name, TreeItemCollapsibleState.Collapsed, @@ -216,7 +253,7 @@ export class ProjectEnvironment implements ProjectTreeItem { this.treeItem = item; } - static getId(workspace: ProjectItem, environment: PythonEnvironment): string { + getId(workspace: ProjectItem, environment: PythonEnvironment): string { return `${workspace.id}>>>${environment.envId}`; } } @@ -226,15 +263,16 @@ export class NoProjectEnvironment implements ProjectTreeItem { public readonly id: string; public readonly treeItem: TreeItem; constructor( - public readonly project: PythonProject, + public readonly project: PythonProject | undefined, public readonly parent: ProjectItem, + private readonly label: string, private readonly description?: string, private readonly tooltip?: string | MarkdownString, private readonly iconPath?: string | IconPath, ) { const randomStr1 = Math.random().toString(36).substring(2); this.id = `${this.parent.id}>>>none>>>${randomStr1}`; - const item = new TreeItem('Please select an environment', TreeItemCollapsibleState.None); + const item = new TreeItem(this.label, TreeItemCollapsibleState.None); item.contextValue = 'no-environment'; item.description = this.description; item.tooltip = this.tooltip; @@ -242,30 +280,12 @@ export class NoProjectEnvironment implements ProjectTreeItem { item.command = { command: 'python-envs.set', title: 'Set Environment', - arguments: [this.project.uri], + arguments: this.project ? [this.project.uri] : undefined, }; this.treeItem = item; } } -export class ProjectPackageRootTreeItem implements ProjectTreeItem { - public readonly kind = ProjectTreeItemKind.packageRoot; - public readonly id: string; - public readonly treeItem: TreeItem; - constructor( - public readonly parent: ProjectEnvironment, - public readonly manager: InternalPackageManager, - public readonly environment: PythonEnvironment, - ) { - const item = new TreeItem('Packages', TreeItemCollapsibleState.Collapsed); - this.id = `${this.parent.id}>>>packages`; - item.contextValue = 'python-package-root'; - item.description = manager.displayName; - item.tooltip = 'Packages installed in this environment'; - this.treeItem = item; - } -} - export class NoPackagesEnvironment implements ProjectTreeItem { public readonly kind = ProjectTreeItemKind.none; public readonly id: string; @@ -323,7 +343,7 @@ export class ProjectPackage implements ProjectTreeItem { public readonly id: string; public readonly treeItem: TreeItem; constructor( - public readonly parent: ProjectPackageRootTreeItem, + public readonly parent: ProjectEnvironment, public readonly pkg: Package, public readonly manager: InternalPackageManager, ) { @@ -336,7 +356,7 @@ export class ProjectPackage implements ProjectTreeItem { this.treeItem = item; } - static getId(projectEnv: ProjectPackageRootTreeItem, pkg: Package): string { + static getId(projectEnv: ProjectEnvironment, pkg: Package): string { return `${projectEnv.id}>>>${pkg.pkgId}`; } } @@ -346,7 +366,7 @@ export class ProjectPackageRootInfoTreeItem implements ProjectTreeItem { public readonly id: string; public readonly treeItem: TreeItem; constructor( - public readonly parent: ProjectPackageRootTreeItem, + public readonly parent: ProjectEnvironment, name: string, description?: string, tooltip?: string | MarkdownString, @@ -362,7 +382,7 @@ export class ProjectPackageRootInfoTreeItem implements ProjectTreeItem { this.treeItem.iconPath = iconPath; this.treeItem.command = command; } - static getId(projectEnv: ProjectPackageRootTreeItem, name: string): string { + static getId(projectEnv: ProjectEnvironment, name: string): string { return `${projectEnv.id}>>>${name}`; } } diff --git a/src/features/views/utils.ts b/src/features/views/utils.ts new file mode 100644 index 0000000..6f03314 --- /dev/null +++ b/src/features/views/utils.ts @@ -0,0 +1,12 @@ +import * as path from 'path'; +import { PythonProject } from '../../api'; +import { getWorkspaceFolder } from '../../common/workspace.apis'; + +export function removable(project: PythonProject): boolean { + const workspace = getWorkspaceFolder(project.uri); + if (workspace) { + // If the project path is same as the workspace path, then we cannot remove the project. + return path.normalize(workspace?.uri.fsPath).toLowerCase() !== path.normalize(project.uri.fsPath).toLowerCase(); + } + return true; +} diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..3a5992f --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,294 @@ +import { ExtensionContext, extensions, QuickInputButtons, Uri, window, workspace } from 'vscode'; +import { PythonEnvironment, PythonEnvironmentApi } from './api'; +import { traceError, traceInfo, traceWarn } from './common/logging'; +import { normalizePath } from './common/utils/pathUtils'; +import { isWindows } from './common/utils/platformUtils'; +import { createTerminal, showInputBoxWithButtons } from './common/window.apis'; +import { getConfiguration } from './common/workspace.apis'; +import { getAutoActivationType } from './features/terminal/utils'; +import { EnvironmentManagers, PythonProjectManager } from './internal.api'; +import { getNativePythonToolsPath, NativeEnvInfo, NativePythonFinder } from './managers/common/nativePythonFinder'; + +/** + * Collects relevant Python environment information for issue reporting + */ +export async function collectEnvironmentInfo( + context: ExtensionContext, + envManagers: EnvironmentManagers, + projectManager: PythonProjectManager, +): Promise { + const info: string[] = []; + + try { + // Extension version + const extensionVersion = context.extension?.packageJSON?.version || 'unknown'; + info.push(`Extension Version: ${extensionVersion}`); + + // Python extension version + const pythonExtension = extensions.getExtension('ms-python.python'); + const pythonVersion = pythonExtension?.packageJSON?.version || 'not installed'; + info.push(`Python Extension Version: ${pythonVersion}`); + + // Environment managers + const managers = envManagers.managers; + info.push(`\nRegistered Environment Managers (${managers.length}):`); + managers.forEach((manager) => { + info.push(` - ${manager.id} (${manager.displayName})`); + }); + + // Available environments + const allEnvironments: PythonEnvironment[] = []; + for (const manager of managers) { + try { + const envs = await manager.getEnvironments('all'); + allEnvironments.push(...envs); + } catch (err) { + info.push(` Error getting environments from ${manager.id}: ${err}`); + } + } + + info.push(`\nTotal Available Environments: ${allEnvironments.length}`); + if (allEnvironments.length > 0) { + info.push('Environment Details:'); + allEnvironments.slice(0, 10).forEach((env, index) => { + info.push(` ${index + 1}. ${env.displayName} (${env.version}) - ${env.displayPath}`); + }); + if (allEnvironments.length > 10) { + info.push(` ... and ${allEnvironments.length - 10} more environments`); + } + } + + // Python projects + const projects = projectManager.getProjects(); + info.push(`\nPython Projects (${projects.length}):`); + for (let index = 0; index < projects.length; index++) { + const project = projects[index]; + info.push(` ${index + 1}. ${project.uri.fsPath}`); + try { + const env = await envManagers.getEnvironment(project.uri); + if (env) { + info.push(` Environment: ${env.displayName}`); + } + } catch (err) { + info.push(` Error getting environment: ${err}`); + } + } + + // Current settings (non-sensitive) + const config = workspace.getConfiguration('python-envs'); + const pyConfig = workspace.getConfiguration('python'); + info.push('\nExtension Settings:'); + info.push(` Default Environment Manager: ${config.get('defaultEnvManager')}`); + info.push(` Default Package Manager: ${config.get('defaultPackageManager')}`); + const pyenvAct = config.get('terminal.autoActivationType', undefined); + const pythonAct = pyConfig.get('terminal.activateEnvironment', undefined); + info.push( + `Auto-activation is "${getAutoActivationType()}". Activation based on first 'py-env.terminal.autoActivationType' setting which is '${pyenvAct}' and 'python.terminal.activateEnvironment' if the first is undefined which is '${pythonAct}'.\n`, + ); + } catch (err) { + info.push(`\nError collecting environment information: ${err}`); + } + + return info.join('\n'); +} + +/** + * Logs the values of defaultPackageManager and defaultEnvManager at all configuration levels (workspace folder, workspace, user/global, default). + */ +export function getEnvManagerAndPackageManagerConfigLevels() { + const config = getConfiguration('python-envs'); + const envManagerInspect = config.inspect('defaultEnvManager'); + const pkgManagerInspect = config.inspect('defaultPackageManager'); + + return { + section: 'Python Envs Configuration Levels', + defaultEnvManager: { + workspaceFolderValue: envManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: envManagerInspect?.workspaceValue ?? 'undefined', + globalValue: envManagerInspect?.globalValue ?? 'undefined', + defaultValue: envManagerInspect?.defaultValue ?? 'undefined', + }, + defaultPackageManager: { + workspaceFolderValue: pkgManagerInspect?.workspaceFolderValue ?? 'undefined', + workspaceValue: pkgManagerInspect?.workspaceValue ?? 'undefined', + globalValue: pkgManagerInspect?.globalValue ?? 'undefined', + defaultValue: pkgManagerInspect?.defaultValue ?? 'undefined', + }, + }; +} + +/** + * Returns the user-configured value for a configuration key if set at any level (workspace folder, workspace, or global), + * otherwise returns undefined. + */ +export function getUserConfiguredSetting(section: string, key: string): T | undefined { + const config = getConfiguration(section); + const inspect = config.inspect(key); + if (!inspect) { + return undefined; + } + if (inspect.workspaceFolderValue !== undefined) { + return inspect.workspaceFolderValue; + } + if (inspect.workspaceValue !== undefined) { + return inspect.workspaceValue; + } + if (inspect.globalValue !== undefined) { + return inspect.globalValue; + } + return undefined; +} + +/** + * Runs the Python Environment Tool (PET) in a terminal window, allowing users to + * execute various PET commands like finding all Python environments or resolving + * the details of a specific environment. + * + * + * @returns A Promise that resolves when the PET command has been executed or cancelled + */ +export async function runPetInTerminalImpl(): Promise { + const petPath = await getNativePythonToolsPath(); + + // Show quick pick menu for PET operation selection + const selectedOption = await window.showQuickPick( + [ + { + label: 'Find All Environments', + description: 'Finds all environments and reports them to the standard output', + detail: 'Runs: pet find --verbose', + }, + { + label: 'Resolve Environment...', + description: 'Resolves & reports the details of the environment to the standard output', + detail: 'Runs: pet resolve ', + }, + ], + { + placeHolder: 'Select a Python Environment Tool (PET) operation', + ignoreFocusOut: true, + }, + ); + + if (!selectedOption) { + return; // User cancelled + } + + if (selectedOption.label === 'Find All Environments') { + // Create and show terminal immediately for 'Find All Environments' option + const terminal = createTerminal({ + name: 'Python Environment Tool (PET)', + }); + terminal.show(); + + // Run pet find --verbose + terminal.sendText(`"${petPath}" find --verbose`, true); + traceInfo(`Running PET find command: ${petPath} find --verbose`); + } else if (selectedOption.label === 'Resolve Environment...') { + try { + // Show input box for path with back button + const placeholder = isWindows() ? 'C:\\path\\to\\python\\executable' : '/path/to/python/executable'; + const inputPath = await showInputBoxWithButtons({ + prompt: 'Enter the path to the Python executable to resolve', + placeHolder: placeholder, + ignoreFocusOut: true, + showBackButton: true, + validateInput: (value) => { + if (!value || value.trim().length === 0) { + return 'Please enter a valid path'; + } + return null; + }, + }); + + if (inputPath) { + // Only create and show terminal after path has been entered + const terminal = createTerminal({ + name: 'Python Environment Tool (PET)', + }); + terminal.show(); + + // Run pet resolve with the provided path + terminal.sendText(`"${petPath}" resolve "${inputPath.trim()}"`, true); + traceInfo(`Running PET resolve command: ${petPath} resolve "${inputPath.trim()}"`); + } + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // If back button was clicked, restart the flow + await runPetInTerminalImpl(); + return; + } + throw ex; // Re-throw other errors + } + } +} + +/** + * Sets the default Python interpreter for the workspace if the user has not explicitly set 'defaultEnvManager'. + * @param nativeFinder - used to resolve interpreter paths. + * @param envManagers - contains all registered managers. + * @param api - The PythonEnvironmentApi for environment resolution and setting. + */ +export async function resolveDefaultInterpreter( + nativeFinder: NativePythonFinder, + envManagers: EnvironmentManagers, + api: PythonEnvironmentApi, +) { + const userSetdefaultInterpreter = getUserConfiguredSetting('python', 'defaultInterpreterPath'); + const userSetDefaultManager = getUserConfiguredSetting('python-envs', 'defaultEnvManager'); + traceInfo( + `[resolveDefaultInterpreter] User configured defaultInterpreterPath: ${userSetdefaultInterpreter} and defaultEnvManager: ${userSetDefaultManager}`, + ); + + // Only proceed if the user has explicitly set defaultInterpreterPath but nothing is saved for defaultEnvManager + if (userSetdefaultInterpreter && !userSetDefaultManager) { + try { + const resolved: NativeEnvInfo = await nativeFinder.resolve(userSetdefaultInterpreter); + if (resolved && resolved.executable) { + if (normalizePath(resolved.executable) === normalizePath(userSetdefaultInterpreter)) { + // no action required, the path is already correct + return; + } + const resolvedEnv = await api.resolveEnvironment(Uri.file(resolved.executable)); + traceInfo(`[resolveDefaultInterpreter] API resolved environment: ${JSON.stringify(resolvedEnv)}`); + + let findEnvManager = envManagers.managers.find((m) => m.id === resolvedEnv?.envId.managerId); + if (!findEnvManager) { + findEnvManager = envManagers.managers.find((m) => m.id === 'ms-python.python:system'); + } + const randomString = Math.random().toString(36).substring(2, 15); + if (resolvedEnv) { + const newEnv: PythonEnvironment = { + envId: { + id: `${userSetdefaultInterpreter}_${randomString}`, + managerId: resolvedEnv?.envId.managerId ?? '', + }, + name: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + displayName: 'defaultInterpreterPath: ' + (resolved.version ?? ''), + version: resolved.version ?? '', + displayPath: userSetdefaultInterpreter ?? '', + environmentPath: userSetdefaultInterpreter ? Uri.file(userSetdefaultInterpreter) : Uri.file(''), + sysPrefix: resolved.arch ?? '', + execInfo: { + run: { + executable: userSetdefaultInterpreter ?? '', + }, + }, + }; + if (workspace.workspaceFolders?.[0] && findEnvManager) { + traceInfo( + `[resolveDefaultInterpreter] Setting environment for workspace: ${workspace.workspaceFolders[0].uri.fsPath}`, + ); + await api.setEnvironment(workspace.workspaceFolders[0].uri, newEnv); + } + } + } else { + traceWarn( + `[resolveDefaultInterpreter] NativeFinder did not resolve an executable for path: ${userSetdefaultInterpreter}`, + ); + } + } catch (err) { + traceError(`[resolveDefaultInterpreter] Error resolving default interpreter: ${err}`); + } + } +} diff --git a/src/internal.api.ts b/src/internal.api.ts index d43e3bf..ce258e3 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -1,31 +1,35 @@ -import { Disposable, Event, LogOutputChannel, MarkdownString, ThemeIcon, Uri } from 'vscode'; +import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, Uri } from 'vscode'; import { - PythonEnvironment, - EnvironmentManager, - PackageManager, - Package, - IconPath, + CreateEnvironmentOptions, + CreateEnvironmentScope, DidChangeEnvironmentEventArgs, DidChangeEnvironmentsEventArgs, DidChangePackagesEventArgs, - PythonProject, - RefreshEnvironmentsScope, - GetEnvironmentsScope, - CreateEnvironmentScope, - SetEnvironmentScope, + EnvironmentGroupInfo, + EnvironmentManager, GetEnvironmentScope, - PythonEnvironmentId, - PythonEnvironmentExecutionInfo, - PythonEnvironmentInfo, + GetEnvironmentsScope, + IconPath, + Package, PackageChangeKind, PackageId, PackageInfo, + PackageManagementOptions, + PackageManager, + PythonEnvironment, + PythonEnvironmentExecutionInfo, + PythonEnvironmentId, + PythonEnvironmentInfo, + PythonProject, PythonProjectCreator, + QuickCreateConfig, + RefreshEnvironmentsScope, ResolveEnvironmentContext, - PackageInstallOptions, - Installable, + SetEnvironmentScope, } from './api'; import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError'; +import { EventNames } from './common/telemetry/constants'; +import { sendTelemetryEvent } from './common/telemetry/sender'; export type EnvironmentManagerScope = undefined | string | Uri | PythonEnvironment; export type PackageManagerScope = undefined | string | Uri | PythonEnvironment | Package; @@ -71,8 +75,27 @@ export interface EnvironmentManagers extends Disposable { registerEnvironmentManager(manager: EnvironmentManager): Disposable; registerPackageManager(manager: PackageManager): Disposable; + /** + * This event is fired when any environment manager changes its collection of environments. + * This can be any environment manager even if it is not the one selected by the user for the workspace. + */ onDidChangeEnvironments: Event; + + /** + * This event is fired when an environment manager changes the environment for + * a particular scope (global, uri, workspace, etc). This can be any environment manager even if it is not the + * one selected by the user for the workspace. It is also fired if the change + * involves unselected to selected or selected to unselected. + */ onDidChangeEnvironment: Event; + + /** + * This event is fired when a selected environment manager changes the environment + * for a particular scope (global, uri, workspace, etc). This is also only fired if + * the previous and current environments are different. It is also fired if the change + * involves unselected to selected or selected to unselected. + */ + onDidChangeEnvironmentFiltered: Event; onDidChangePackages: Event; onDidChangeEnvironmentManager: Event; @@ -85,6 +108,13 @@ export interface EnvironmentManagers extends Disposable { packageManagers: InternalPackageManager[]; clearCache(scope: EnvironmentManagerScope): Promise; + + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + setEnvironments(scope: Uri[] | string, environment?: PythonEnvironment): Promise; + setEnvironmentsIfUnset(scope: Uri[] | string, environment?: PythonEnvironment): Promise; + getEnvironment(scope: GetEnvironmentScope): Promise; + + getProjectEnvManagers(uris: Uri[]): InternalEnvironmentManager[]; } export class InternalEnvironmentManager implements EnvironmentManager { @@ -116,14 +146,28 @@ export class InternalEnvironmentManager implements EnvironmentManager { return this.manager.create !== undefined; } - create(scope: CreateEnvironmentScope): Promise { + create( + scope: CreateEnvironmentScope, + options: CreateEnvironmentOptions | undefined, + ): Promise { if (this.manager.create) { - return this.manager.create(scope); + return this.manager.create(scope, options); } return Promise.reject(new CreateEnvironmentNotSupported(`Create Environment not supported by: ${this.id}`)); } + public get supportsQuickCreate(): boolean { + return this.manager.quickCreateConfig !== undefined && this.manager.create !== undefined; + } + + quickCreateConfig(): QuickCreateConfig | undefined { + if (this.manager.quickCreateConfig && this.manager.create) { + return this.manager.quickCreateConfig(); + } + throw new CreateEnvironmentNotSupported(`Quick Create Environment not supported by: ${this.id}`); + } + public get supportsRemove(): boolean { return this.manager.remove !== undefined; } @@ -196,34 +240,39 @@ export class InternalPackageManager implements PackageManager { public get iconPath(): IconPath | undefined { return this.manager.iconPath; } - public get logOutput(): LogOutputChannel | undefined { - return this.manager.logOutput; + public get log(): LogOutputChannel | undefined { + return this.manager.log; } - install(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise { - return this.manager.install(environment, packages, options); - } - uninstall(environment: PythonEnvironment, packages: Package[] | string[]): Promise { - return this.manager.uninstall(environment, packages); + async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { + try { + await this.manager.manage(environment, options); + sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, { managerId: this.id, result: 'success' }); + } catch (error) { + if (error instanceof CancellationError) { + sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, { + managerId: this.id, + result: 'cancelled', + }); + throw error; + } + sendTelemetryEvent(EventNames.PACKAGE_MANAGEMENT, undefined, { managerId: this.id, result: 'error' }); + throw error; + } } + refresh(environment: PythonEnvironment): Promise { return this.manager.refresh(environment); } + getPackages(environment: PythonEnvironment): Promise { return this.manager.getPackages(environment); } - public get supportsGetInstallable(): boolean { - return this.manager.getInstallable !== undefined; - } - - getInstallable(environment: PythonEnvironment): Promise { - return this.manager.getInstallable ? this.manager.getInstallable(environment) : Promise.resolve([]); - } - onDidChangePackages(handler: (e: DidChangePackagesEventArgs) => void): Disposable { return this.manager.onDidChangePackages ? this.manager.onDidChangePackages(handler) : new Disposable(() => {}); } + equals(other: PackageManager): boolean { return this.manager === other; } @@ -236,9 +285,9 @@ export interface PythonProjectManager extends Disposable { uri: Uri, options?: { description?: string; tooltip?: string | MarkdownString; iconPath?: IconPath }, ): PythonProject; - add(pyWorkspace: PythonProject | PythonProject[]): void; + add(pyWorkspace: PythonProject | PythonProject[]): Promise; remove(pyWorkspace: PythonProject | PythonProject[]): void; - getProjects(): ReadonlyArray; + getProjects(uris?: Uri[]): ReadonlyArray; get(uri: Uri): PythonProject | undefined; onDidChangeProjects: Event; } @@ -247,6 +296,7 @@ export interface PythonProjectSettings { path: string; envManager: string; packageManager: string; + workspace?: string; } export class PythonEnvironmentImpl implements PythonEnvironment { @@ -259,8 +309,9 @@ export class PythonEnvironmentImpl implements PythonEnvironment { public readonly description?: string; public readonly tooltip?: string | MarkdownString; public readonly iconPath?: IconPath; - public readonly execInfo?: PythonEnvironmentExecutionInfo; + public readonly execInfo: PythonEnvironmentExecutionInfo; public readonly sysPrefix: string; + public readonly group?: string | EnvironmentGroupInfo; constructor(public readonly envId: PythonEnvironmentId, info: PythonEnvironmentInfo) { this.name = info.name; @@ -274,6 +325,7 @@ export class PythonEnvironmentImpl implements PythonEnvironment { this.iconPath = info.iconPath; this.execInfo = info.execInfo; this.sysPrefix = info.sysPrefix; + this.group = info.group; } } @@ -313,7 +365,7 @@ export class PythonProjectsImpl implements PythonProject { this.uri = uri; this.description = options?.description ?? uri.fsPath; this.tooltip = options?.tooltip ?? uri.fsPath; - this.iconPath = options?.iconPath ?? ThemeIcon.Folder; + this.iconPath = options?.iconPath; } } @@ -321,17 +373,3 @@ export interface ProjectCreators extends Disposable { registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; getProjectCreators(): PythonProjectCreator[]; } - -export interface PythonTerminalExecutionOptions { - project: PythonProject; - args: string[]; - useDedicatedTerminal?: Uri; -} - -export interface PythonTaskExecutionOptions { - project: PythonProject; - args: string[]; - cwd?: string; - env?: { [key: string]: string }; - name: string; -} diff --git a/src/managers/builtin/cache.ts b/src/managers/builtin/cache.ts new file mode 100644 index 0000000..08f88d0 --- /dev/null +++ b/src/managers/builtin/cache.ts @@ -0,0 +1,58 @@ +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { getWorkspacePersistentState } from '../../common/persistentState'; + +export const SYSTEM_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:system:WORKSPACE_SELECTED`; +export const SYSTEM_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:system:GLOBAL_SELECTED`; + +export async function clearSystemEnvCache(): Promise { + const keys = [SYSTEM_WORKSPACE_KEY, SYSTEM_GLOBAL_KEY]; + const state = await getWorkspacePersistentState(); + await state.clear(keys); +} + +export async function getSystemEnvForWorkspace(fsPath: string): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(SYSTEM_WORKSPACE_KEY); + if (data) { + try { + return data[fsPath]; + } catch { + return undefined; + } + } + return undefined; +} + +export async function setSystemEnvForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(SYSTEM_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(SYSTEM_WORKSPACE_KEY, data); +} + +export async function setSystemEnvForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(SYSTEM_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(SYSTEM_WORKSPACE_KEY, data); +} + +export async function getSystemEnvForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + return await state.get(SYSTEM_GLOBAL_KEY); +} + +export async function setSystemEnvForGlobal(envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(SYSTEM_GLOBAL_KEY, envPath); +} diff --git a/src/managers/builtin/helpers.ts b/src/managers/builtin/helpers.ts new file mode 100644 index 0000000..4f71b9b --- /dev/null +++ b/src/managers/builtin/helpers.ts @@ -0,0 +1,95 @@ +import * as ch from 'child_process'; +import { CancellationError, CancellationToken, LogOutputChannel } from 'vscode'; +import { createDeferred } from '../../common/utils/deferred'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; +import { EventNames } from '../../common/telemetry/constants'; + +const available = createDeferred(); +export async function isUvInstalled(log?: LogOutputChannel): Promise { + if (available.completed) { + return available.promise; + } + log?.info(`Running: uv --version`); + const proc = ch.spawn('uv', ['--version']); + proc.on('error', () => { + available.resolve(false); + }); + proc.stdout.on('data', (d) => log?.info(d.toString())); + proc.on('exit', (code) => { + if (code === 0) { + sendTelemetryEvent(EventNames.VENV_USING_UV); + } + available.resolve(code === 0); + }); + return available.promise; +} + +export async function runUV( + args: string[], + cwd?: string, + log?: LogOutputChannel, + token?: CancellationToken, +): Promise { + log?.info(`Running: uv ${args.join(' ')}`); + return new Promise((resolve, reject) => { + const proc = ch.spawn('uv', args, { cwd: cwd }); + token?.onCancellationRequested(() => { + proc.kill(); + reject(new CancellationError()); + }); + + let builder = ''; + proc.stdout?.on('data', (data) => { + const s = data.toString('utf-8'); + builder += s; + log?.append(s); + }); + proc.stderr?.on('data', (data) => { + log?.append(data.toString('utf-8')); + }); + proc.on('close', () => { + resolve(builder); + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Failed to run uv ${args.join(' ')}`)); + } + }); + }); +} + +export async function runPython( + python: string, + args: string[], + cwd?: string, + log?: LogOutputChannel, + token?: CancellationToken, +): Promise { + log?.info(`Running: ${python} ${args.join(' ')}`); + return new Promise((resolve, reject) => { + const proc = ch.spawn(python, args, { cwd: cwd }); + token?.onCancellationRequested(() => { + proc.kill(); + reject(new CancellationError()); + }); + let builder = ''; + proc.stdout?.on('data', (data) => { + const s = data.toString('utf-8'); + builder += s; + log?.append(`python: ${s}`); + }); + proc.stderr?.on('data', (data) => { + const s = data.toString('utf-8'); + builder += s; + log?.append(`python: ${s}`); + }); + proc.on('close', () => { + resolve(builder); + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Failed to run python ${args.join(' ')}`)); + } + }); + }); +} diff --git a/src/managers/builtin/main.ts b/src/managers/builtin/main.ts new file mode 100644 index 0000000..d1f4a40 --- /dev/null +++ b/src/managers/builtin/main.ts @@ -0,0 +1,57 @@ +import { Disposable, LogOutputChannel } from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { createSimpleDebounce } from '../../common/utils/debounce'; +import { onDidEndTerminalShellExecution } from '../../common/window.apis'; +import { createFileSystemWatcher, onDidDeleteFiles } from '../../common/workspace.apis'; +import { getPythonApi } from '../../features/pythonApi'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { PipPackageManager } from './pipManager'; +import { isPipInstallCommand } from './pipUtils'; +import { SysPythonManager } from './sysPythonManager'; +import { VenvManager } from './venvManager'; + +export async function registerSystemPythonFeatures( + nativeFinder: NativePythonFinder, + disposables: Disposable[], + log: LogOutputChannel, + envManager: SysPythonManager, +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + const venvManager = new VenvManager(nativeFinder, api, envManager, log); + const pkgManager = new PipPackageManager(api, log, venvManager); + + disposables.push( + api.registerPackageManager(pkgManager), + api.registerEnvironmentManager(envManager), + api.registerEnvironmentManager(venvManager), + ); + + const venvDebouncedRefresh = createSimpleDebounce(500, () => { + venvManager.watcherRefresh(); + }); + const watcher = createFileSystemWatcher('{**/activate}', false, true, false); + disposables.push( + watcher, + watcher.onDidCreate(() => { + venvDebouncedRefresh.trigger(); + }), + watcher.onDidDelete(() => { + venvDebouncedRefresh.trigger(); + }), + onDidDeleteFiles(() => { + venvDebouncedRefresh.trigger(); + }), + ); + + disposables.push( + onDidEndTerminalShellExecution(async (e) => { + const cwd = e.terminal.shellIntegration?.cwd; + if (isPipInstallCommand(e.execution.commandLine.value) && cwd) { + const env = await venvManager.get(cwd); + if (env) { + await pkgManager.refresh(env); + } + } + }), + ); +} diff --git a/src/managers/builtin/pipListUtils.ts b/src/managers/builtin/pipListUtils.ts new file mode 100644 index 0000000..3d8d794 --- /dev/null +++ b/src/managers/builtin/pipListUtils.ts @@ -0,0 +1,37 @@ +export interface PipPackage { + name: string; + version: string; + displayName: string; + description: string; +} +export function isValidVersion(version: string): boolean { + return /^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$/.test( + version, + ); +} +export function parsePipList(data: string): PipPackage[] { + const collection: PipPackage[] = []; + + const lines = data.split('\n').splice(2); + for (let line of lines) { + if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { + continue; + } + const parts = line.split(' ').filter((e) => e); + if (parts.length === 2) { + const name = parts[0].trim(); + const version = parts[1].trim(); + if (!isValidVersion(version)) { + continue; + } + const pkg = { + name, + version, + displayName: name, + description: version, + }; + collection.push(pkg); + } + } + return collection; +} diff --git a/src/managers/sysPython/pipManager.ts b/src/managers/builtin/pipManager.ts similarity index 52% rename from src/managers/sysPython/pipManager.ts rename to src/managers/builtin/pipManager.ts index 0ed884b..da52027 100644 --- a/src/managers/sysPython/pipManager.ts +++ b/src/managers/builtin/pipManager.ts @@ -1,22 +1,27 @@ -import * as path from 'path'; -import { Event, EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, Uri, window } from 'vscode'; +import { + CancellationError, + Event, + EventEmitter, + LogOutputChannel, + MarkdownString, + ProgressLocation, + ThemeIcon, + window, +} from 'vscode'; import { DidChangePackagesEventArgs, IconPath, - Installable, Package, PackageChangeKind, - PackageInstallOptions, + PackageManagementOptions, PackageManager, PythonEnvironment, PythonEnvironmentApi, } from '../../api'; -import { installPackages, refreshPackages, uninstallPackages } from './utils'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { managePackages, refreshPackages } from './utils'; import { Disposable } from 'vscode-jsonrpc'; -import { getProjectInstallable } from './venvUtils'; -import { pickProject } from '../../common/pickers'; import { VenvManager } from './venvManager'; +import { getWorkspacePackagesToInstall } from './pipUtils'; function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { const changes: { kind: PackageChangeKind; pkg: Package }[] = []; @@ -37,14 +42,14 @@ export class PipPackageManager implements PackageManager, Disposable { constructor( private readonly api: PythonEnvironmentApi, - public readonly logOutput: LogOutputChannel, + public readonly log: LogOutputChannel, private readonly venv: VenvManager, ) { this.name = 'pip'; this.displayName = 'Pip'; this.description = 'This package manager for python installs using pip.'; this.tooltip = new MarkdownString('This package manager for python installs using `pip`.'); - this.iconPath = Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')); + this.iconPath = new ThemeIcon('python'); } readonly name: string; readonly displayName?: string; @@ -52,53 +57,51 @@ export class PipPackageManager implements PackageManager, Disposable { readonly tooltip?: string | MarkdownString; readonly iconPath?: IconPath; - async install(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise { + async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { + let toInstall: string[] = [...(options.install ?? [])]; + let toUninstall: string[] = [...(options.uninstall ?? [])]; + + if (toInstall.length === 0 && toUninstall.length === 0) { + const projects = this.venv.getProjectsByEnvironment(environment); + const result = await getWorkspacePackagesToInstall(this.api, options, projects, environment, this.log); + if (result) { + toInstall = result.install; + toUninstall = result.uninstall; + } else { + return; + } + } + + const manageOptions = { + ...options, + install: toInstall, + uninstall: toUninstall, + }; await window.withProgress( { location: ProgressLocation.Notification, title: 'Installing packages', + cancellable: true, }, - async () => { + async (_progress, token) => { try { const before = this.packages.get(environment.envId.id) ?? []; - const after = await installPackages(environment, packages, options, this.api, this); + const after = await managePackages(environment, manageOptions, this.api, this, token); const changes = getChanges(before, after); this.packages.set(environment.envId.id, after); this._onDidChangePackages.fire({ environment, manager: this, changes }); } catch (e) { - this.logOutput.error('Error installing packages', e); - setImmediate(async () => { - const result = await window.showErrorMessage('Error installing packages', 'View Output'); - if (result === 'View Output') { - this.logOutput.show(); - } - }); - } - }, - ); - } - - async uninstall(environment: PythonEnvironment, packages: Package[] | string[]): Promise { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Uninstalling packages', - }, - async () => { - try { - const before = this.packages.get(environment.envId.id) ?? []; - const after = await uninstallPackages(environment, this.api, this, packages); - const changes = getChanges(before, after); - this.packages.set(environment.envId.id, after); - this._onDidChangePackages.fire({ environment: environment, manager: this, changes }); - } catch (e) { - this.logOutput.error('Error uninstalling packages', e); + if (e instanceof CancellationError) { + throw e; + } + this.log.error('Error managing packages', e); setImmediate(async () => { - const result = await window.showErrorMessage('Error installing packages', 'View Output'); + const result = await window.showErrorMessage('Error managing packages', 'View Output'); if (result === 'View Output') { - this.logOutput.show(); + this.log.show(); } }); + throw e; } }, ); @@ -111,7 +114,13 @@ export class PipPackageManager implements PackageManager, Disposable { title: 'Refreshing packages', }, async () => { - this.packages.set(environment.envId.id, await refreshPackages(environment, this.api, this)); + const before = this.packages.get(environment.envId.id) ?? []; + const after = await refreshPackages(environment, this.api, this); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + if (changes.length > 0) { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } }, ); } @@ -121,23 +130,7 @@ export class PipPackageManager implements PackageManager, Disposable { } return this.packages.get(environment.envId.id); } - async getInstallable(environment: PythonEnvironment): Promise { - const projects = this.venv.getProjectsByEnvironment(environment); - if (projects.length === 0) { - return []; - } - - if (projects.length === 1) { - return getProjectInstallable(this.api, projects[0]); - } - - const project = await pickProject(projects); - if (!project) { - return []; - } - return getProjectInstallable(this.api, project); - } dispose(): void { this._onDidChangePackages.dispose(); this.packages.clear(); diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts new file mode 100644 index 0000000..d221f5a --- /dev/null +++ b/src/managers/builtin/pipUtils.ts @@ -0,0 +1,237 @@ +import * as tomljs from '@iarna/toml'; +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { LogOutputChannel, ProgressLocation, QuickInputButtons, QuickPickItem, Uri } from 'vscode'; +import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi, PythonProject } from '../../api'; +import { EXTENSION_ROOT_DIR } from '../../common/constants'; +import { PackageManagement, Pickers, VenvManagerStrings } from '../../common/localize'; +import { traceInfo } from '../../common/logging'; +import { showQuickPickWithButtons, withProgress } from '../../common/window.apis'; +import { findFiles } from '../../common/workspace.apis'; +import { selectFromCommonPackagesToInstall, selectFromInstallableToInstall } from '../common/pickers'; +import { Installable } from '../common/types'; +import { mergePackages } from '../common/utils'; +import { refreshPipPackages } from './utils'; + +async function tomlParse(fsPath: string, log?: LogOutputChannel): Promise { + try { + const content = await fse.readFile(fsPath, 'utf-8'); + return tomljs.parse(content); + } catch (err) { + log?.error('Failed to parse `pyproject.toml`:', err); + } + return {}; +} + +function isPipInstallableToml(toml: tomljs.JsonMap): boolean { + return toml['build-system'] !== undefined && toml.project !== undefined; +} + +function getTomlInstallable(toml: tomljs.JsonMap, tomlPath: Uri): Installable[] { + const extras: Installable[] = []; + const projectDir = path.dirname(tomlPath.fsPath); + + if (isPipInstallableToml(toml)) { + const name = path.basename(tomlPath.fsPath); + extras.push({ + name, + displayName: name, + description: VenvManagerStrings.installEditable, + group: 'TOML', + args: ['-e', projectDir], + uri: tomlPath, + }); + } + + if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { + const deps = (toml.project as tomljs.JsonMap)['optional-dependencies']; + for (const key of Object.keys(deps)) { + extras.push({ + name: key, + displayName: key, + group: 'TOML', + // Use a single -e argument with the extras specified as part of the path + args: ['-e', `${projectDir}[${key}]`], + uri: tomlPath, + }); + } + } + return extras; +} + +async function getCommonPackages(): Promise { + try { + const pipData = path.join(EXTENSION_ROOT_DIR, 'files', 'common_pip_packages.json'); + const data = await fse.readFile(pipData, { encoding: 'utf-8' }); + const packages = JSON.parse(data) as { name: string; uri: string }[]; + + return packages.map((p) => { + return { + name: p.name, + displayName: p.name, + uri: Uri.parse(p.uri), + }; + }); + } catch { + return []; + } +} + +async function selectWorkspaceOrCommon( + installable: Installable[], + common: Installable[], + showSkipOption: boolean, + installed: string[], +): Promise { + if (installable.length === 0 && common.length === 0) { + return undefined; + } + + const items: QuickPickItem[] = []; + if (installable.length > 0) { + items.push({ + label: PackageManagement.workspaceDependencies, + description: PackageManagement.workspaceDependenciesDescription, + }); + } + + if (common.length > 0) { + items.push({ + label: PackageManagement.searchCommonPackages, + description: PackageManagement.searchCommonPackagesDescription, + }); + } + + if (showSkipOption && items.length > 0) { + items.push({ label: PackageManagement.skipPackageInstallation }); + } + + let showBackButton = true; + let selected: QuickPickItem[] | QuickPickItem | undefined = undefined; + if (items.length === 1) { + selected = items[0]; + showBackButton = false; + } else { + selected = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Packages.selectOption, + ignoreFocusOut: true, + showBackButton: true, + matchOnDescription: false, + matchOnDetail: false, + }); + } + + if (selected && !Array.isArray(selected)) { + try { + if (selected.label === PackageManagement.workspaceDependencies) { + return await selectFromInstallableToInstall(installable, undefined, { showBackButton }); + } else if (selected.label === PackageManagement.searchCommonPackages) { + return await selectFromCommonPackagesToInstall(common, installed, undefined, { showBackButton }); + } else if (selected.label === PackageManagement.skipPackageInstallation) { + traceInfo('Package Installer: user selected skip package installation'); + return { install: [], uninstall: [] } satisfies PipPackages; + } else { + return undefined; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (ex: any) { + if (ex === QuickInputButtons.Back) { + return selectWorkspaceOrCommon(installable, common, showSkipOption, installed); + } + } + } + return undefined; +} + +export interface PipPackages { + install: string[]; + uninstall: string[]; +} + +export async function getWorkspacePackagesToInstall( + api: PythonEnvironmentApi, + options: PackageManagementOptions, + project?: PythonProject[], + environment?: PythonEnvironment, + log?: LogOutputChannel, +): Promise { + const installable = (await getProjectInstallable(api, project)) ?? []; + let common = await getCommonPackages(); + let installed: string[] | undefined; + if (environment) { + installed = (await refreshPipPackages(environment, log, { showProgress: true }))?.map((pkg) => pkg.name); + common = mergePackages(common, installed ?? []); + } + return selectWorkspaceOrCommon(installable, common, !!options.showSkipOption, installed ?? []); +} + +export async function getProjectInstallable( + api: PythonEnvironmentApi, + projects?: PythonProject[], +): Promise { + if (!projects) { + return []; + } + const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; + const installable: Installable[] = []; + await withProgress( + { + location: ProgressLocation.Notification, + title: VenvManagerStrings.searchingDependencies, + }, + async (_progress, token) => { + const results: Uri[] = ( + await Promise.all([ + findFiles('**/*requirements*.txt', exclude, undefined, token), + findFiles('*requirements*.txt', exclude, undefined, token), + findFiles('**/requirements/*.txt', exclude, undefined, token), + findFiles('**/pyproject.toml', exclude, undefined, token), + ]) + ).flat(); + + // Deduplicate by fsPath + const uniqueResults = Array.from(new Map(results.map((uri) => [uri.fsPath, uri])).values()); + + const fsPaths = projects.map((p) => p.uri.fsPath); + const filtered = uniqueResults + .filter((uri) => { + const p = api.getPythonProject(uri)?.uri.fsPath; + return p && fsPaths.includes(p); + }) + .sort(); + + await Promise.all( + filtered.map(async (uri) => { + if (uri.fsPath.endsWith('.toml')) { + const toml = await tomlParse(uri.fsPath); + installable.push(...getTomlInstallable(toml, uri)); + } else { + const name = path.basename(uri.fsPath); + installable.push({ + name, + uri, + displayName: name, + group: 'Requirements', + args: ['-r', uri.fsPath], + }); + } + }), + ); + }, + ); + return installable; +} + +export function isPipInstallCommand(command: string): boolean { + // Regex to match pip install commands, capturing variations like: + // pip install package + // python -m pip install package + // pip3 install package + // py -m pip install package + // pip install -r requirements.txt + // uv pip install package + // poetry run pip install package + // pipx run pip install package + // Any other tool that might wrap pip install + return /(?:^|\s)(?:\S+\s+)*(?:pip\d*)\s+(install|uninstall)\b/.test(command); +} diff --git a/src/managers/sysPython/sysPythonManager.ts b/src/managers/builtin/sysPythonManager.ts similarity index 63% rename from src/managers/sysPython/sysPythonManager.ts rename to src/managers/builtin/sysPythonManager.ts index c0367ca..505d940 100644 --- a/src/managers/sysPython/sysPythonManager.ts +++ b/src/managers/builtin/sysPythonManager.ts @@ -1,5 +1,5 @@ import * as path from 'path'; -import { EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, Uri, window } from 'vscode'; +import { EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, ThemeIcon, Uri, window } from 'vscode'; import { DidChangeEnvironmentEventArgs, DidChangeEnvironmentsEventArgs, @@ -10,23 +10,24 @@ import { IconPath, PythonEnvironment, PythonEnvironmentApi, + PythonProject, RefreshEnvironmentsScope, ResolveEnvironmentContext, SetEnvironmentScope, } from '../../api'; +import { SysManagerStrings } from '../../common/localize'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { getLatest } from '../common/utils'; import { + clearSystemEnvCache, getSystemEnvForGlobal, getSystemEnvForWorkspace, - refreshPythons, - resolvePythonEnvironment, - resolvePythonEnvironmentPath, setSystemEnvForGlobal, setSystemEnvForWorkspace, -} from './utils'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { NativePythonFinder } from '../common/nativePythonFinder'; -import { createDeferred, Deferred } from '../../common/utils/deferred'; -import { getLatest } from '../common/utils'; + setSystemEnvForWorkspaces, +} from './cache'; +import { refreshPythons, resolveSystemPythonEnvironmentPath } from './utils'; export class SysPythonManager implements EnvironmentManager { private collection: PythonEnvironment[] = []; @@ -42,7 +43,7 @@ export class SysPythonManager implements EnvironmentManager { public readonly name: string; public readonly displayName: string; public readonly preferredPackageManagerId: string; - public readonly description: string; + public readonly description: string | undefined; public readonly tooltip: string | MarkdownString; public readonly iconPath: IconPath; @@ -54,9 +55,9 @@ export class SysPythonManager implements EnvironmentManager { this.name = 'system'; this.displayName = 'Global'; this.preferredPackageManagerId = 'ms-python.python:pip'; - this.description = 'Manages Global python installs'; - this.tooltip = new MarkdownString('$(globe) Python Environment Manager', true); - this.iconPath = Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')); + this.description = undefined; + this.tooltip = new MarkdownString(SysManagerStrings.sysManagerDescription, true); + this.iconPath = new ThemeIcon('globe'); } private _initialized: Deferred | undefined; @@ -67,13 +68,13 @@ export class SysPythonManager implements EnvironmentManager { this._initialized = createDeferred(); - await this.internalRefresh(false, 'Discovering Python environments'); + await this.internalRefresh(false, SysManagerStrings.sysManagerDiscovering); this._initialized.resolve(); } refresh(_scope: RefreshEnvironmentsScope): Promise { - return this.internalRefresh(true, 'Refreshing Python environments'); + return this.internalRefresh(true, SysManagerStrings.sysManagerRefreshing); } private async internalRefresh(hardRefresh: boolean, title: string) { @@ -85,6 +86,7 @@ export class SysPythonManager implements EnvironmentManager { async () => { const discard = this.collection.map((c) => c); + // hit here is fine... this.collection = await refreshPythons(hardRefresh, this.nativeFinder, this.api, this.log, this); await this.loadEnvMap(); @@ -126,71 +128,102 @@ export class SysPythonManager implements EnvironmentManager { } async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + if (scope === undefined) { + this.globalEnv = environment ?? getLatest(this.collection); + if (environment) { + await setSystemEnvForGlobal(environment.environmentPath.fsPath); + } + } + if (scope instanceof Uri) { const pw = this.api.getPythonProject(scope); - if (pw) { - if (environment) { - this.fsPathToEnv.set(pw.uri.fsPath, environment); - await setSystemEnvForWorkspace(pw.uri.fsPath, environment.environmentPath.fsPath); - } else { - this.fsPathToEnv.delete(pw.uri.fsPath); - await setSystemEnvForWorkspace(pw.uri.fsPath, undefined); - } + if (!pw) { + this.log.warn( + `Unable to set environment for ${scope.fsPath}: Not a python project, folder or PEP723 script.`, + this.api.getPythonProjects().map((p) => p.uri.fsPath), + ); return; } - this.log.warn( - `Unable to set environment for ${scope.fsPath}: Not a python project, folder or PEP723 script.`, - this.api.getPythonProjects().map((p) => p.uri.fsPath), - ); - } - if (scope === undefined) { - this.globalEnv = environment ?? getLatest(this.collection); if (environment) { - await setSystemEnvForGlobal(environment.environmentPath.fsPath); + this.fsPathToEnv.set(pw.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(pw.uri.fsPath); } + await setSystemEnvForWorkspace(pw.uri.fsPath, environment?.environmentPath.fsPath); + } + + if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setSystemEnvForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); } } async resolve(context: ResolveEnvironmentContext): Promise { - if (context instanceof Uri) { - // NOTE: `environmentPath` for envs in `this.collection` for venv always points to the python - // executable in the venv. This is set when we create the PythonEnvironment object. - const found = this.findEnvironmentByPath(context.fsPath); - if (found) { - // If it is in the collection, then it is a venv, and it should already be fully resolved. - return found; - } - } else { - // We have received a partially or fully resolved environment. - const found = - this.collection.find((e) => e.envId.id === context.envId.id) ?? - this.findEnvironmentByPath(context.environmentPath.fsPath); - if (found) { - // If it is in the collection, then it is a venv, and it should already be fully resolved. - return found; - } - - if (context.execInfo) { - // This is a fully resolved environment, from venv perspective. - return context; - } + // NOTE: `environmentPath` for envs in `this.collection` for system envs always points to the python + // executable. This is set when we create the PythonEnvironment object. + const found = this.findEnvironmentByPath(context.fsPath); + if (found) { + // If it is in the collection, then it is a venv, and it should already be fully resolved. + return found; } // This environment is unknown. Resolve it. - const resolved = await resolvePythonEnvironment(context, this.nativeFinder, this.api, this); + const resolved = await resolveSystemPythonEnvironmentPath(context.fsPath, this.nativeFinder, this.api, this); if (resolved) { // This is just like finding a new environment or creating a new one. // Add it to collection, and trigger the added event. - this.collection.push(resolved); + + // For all other env types we need to ensure that the environment is of the type managed by the manager. + // But System is a exception, this is the last resort for resolving. So we don't need to check. + // We will just add it and treat it as a non-activatable environment. + const exists = this.collection.some( + (e) => e.environmentPath.toString() === resolved.environmentPath.toString(), + ); + if (!exists) { + // only add it if it is not already in the collection to avoid duplicates + this.collection.push(resolved); + } this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]); } return resolved; } + async clearCache(): Promise { + await clearSystemEnvCache(); + } + private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { - const normalized = path.normalize(fsPath); + const normalized = path.normalize(fsPath); // /opt/homebrew/bin/python3.12 return this.collection.find((e) => { const n = path.normalize(e.environmentPath.fsPath); return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized; @@ -225,7 +258,7 @@ export class SysPythonManager implements EnvironmentManager { // If the environment is not found, resolve the fsPath. if (!this.globalEnv) { - this.globalEnv = await resolvePythonEnvironmentPath(fsPath, this.nativeFinder, this.api, this); + this.globalEnv = await resolveSystemPythonEnvironmentPath(fsPath, this.nativeFinder, this.api, this); // If the environment is resolved, add it to the collection if (this.globalEnv) { @@ -247,16 +280,16 @@ export class SysPythonManager implements EnvironmentManager { const env = await getSystemEnvForWorkspace(p); if (env) { - const found = this.findEnvironmentByPath(p); + const found = this.findEnvironmentByPath(env); if (found) { this.fsPathToEnv.set(p, found); } else { // If not found, resolve the path. - const resolved = await resolvePythonEnvironmentPath(env, this.nativeFinder, this.api, this); + const resolved = await resolveSystemPythonEnvironmentPath(env, this.nativeFinder, this.api, this); if (resolved) { - // If resolved add it to the collection + // If resolved add it to the collection. this.fsPathToEnv.set(p, resolved); this.collection.push(resolved); } else { diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts new file mode 100644 index 0000000..5a87c7b --- /dev/null +++ b/src/managers/builtin/utils.ts @@ -0,0 +1,299 @@ +import { CancellationToken, LogOutputChannel, ProgressLocation, QuickPickItem, Uri, window } from 'vscode'; +import { + EnvironmentManager, + Package, + PackageManagementOptions, + PackageManager, + PythonEnvironment, + PythonEnvironmentApi, + PythonEnvironmentInfo, +} from '../../api'; +import { showErrorMessageWithLogs } from '../../common/errors/utils'; +import { SysManagerStrings } from '../../common/localize'; +import { withProgress } from '../../common/window.apis'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { shortVersion, sortEnvironments } from '../common/utils'; +import { isUvInstalled, runPython, runUV } from './helpers'; +import { parsePipList, PipPackage } from './pipListUtils'; + +function asPackageQuickPickItem(name: string, version?: string): QuickPickItem { + return { + label: name, + description: version, + }; +} + +export async function pickPackages(uninstall: boolean, packages: string[] | Package[]): Promise { + const items = packages.map((pkg) => { + if (typeof pkg === 'string') { + return asPackageQuickPickItem(pkg); + } + return asPackageQuickPickItem(pkg.name, pkg.version); + }); + + const result = await window.showQuickPick(items, { + placeHolder: uninstall ? SysManagerStrings.selectUninstall : SysManagerStrings.selectInstall, + canPickMany: true, + ignoreFocusOut: true, + }); + + if (Array.isArray(result)) { + return result.map((e) => e.label); + } + return []; +} + +function getKindName(kind: NativePythonEnvironmentKind | undefined): string | undefined { + switch (kind) { + case NativePythonEnvironmentKind.homebrew: + return 'homebrew'; + + case NativePythonEnvironmentKind.macXCode: + return 'xcode'; + + case NativePythonEnvironmentKind.windowsStore: + return 'store'; + + case NativePythonEnvironmentKind.macCommandLineTools: + case NativePythonEnvironmentKind.macPythonOrg: + case NativePythonEnvironmentKind.globalPaths: + case NativePythonEnvironmentKind.linuxGlobal: + case NativePythonEnvironmentKind.windowsRegistry: + default: + return undefined; + } +} + +function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo { + if (env.executable && env.version && env.prefix) { + const kindName = getKindName(env.kind); + const sv = shortVersion(env.version); + const name = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`; + const displayName = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`; + const shortDisplayName = kindName ? `${sv} (${kindName})` : `${sv}`; + return { + name: env.name ?? name, + displayName: env.displayName ?? displayName, + shortDisplayName: shortDisplayName, + displayPath: env.executable, + version: env.version, + description: undefined, + tooltip: env.executable, + environmentPath: Uri.file(env.executable), + sysPrefix: env.prefix, + execInfo: { + run: { + executable: env.executable, + args: [], + }, + }, + }; + } else { + throw new Error(`Invalid python info: ${JSON.stringify(env)}`); + } +} + +export async function refreshPythons( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + uris?: Uri[], +): Promise { + const collection: PythonEnvironment[] = []; + const data = await nativeFinder.refresh(hardRefresh, uris); + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter( + (e) => + e.kind === undefined || + (e.kind && + [ + NativePythonEnvironmentKind.globalPaths, + NativePythonEnvironmentKind.homebrew, + NativePythonEnvironmentKind.linuxGlobal, + NativePythonEnvironmentKind.macCommandLineTools, + NativePythonEnvironmentKind.macPythonOrg, + NativePythonEnvironmentKind.macXCode, + NativePythonEnvironmentKind.windowsRegistry, + NativePythonEnvironmentKind.windowsStore, + ].includes(e.kind)), + ); + envs.forEach((env) => { + try { + const envInfo = getPythonInfo(env); + const python = api.createPythonEnvironmentItem(envInfo, manager); + collection.push(python); + } catch (e) { + log.error((e as Error).message); + } + }); + return sortEnvironments(collection); +} + +async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOutputChannel): Promise { + const useUv = await isUvInstalled(); + if (useUv) { + return await runUV(['pip', 'list', '--python', environment.execInfo.run.executable], undefined, log); + } + return await runPython(environment.execInfo.run.executable, ['-m', 'pip', 'list'], undefined, log); +} + +export async function refreshPipPackages( + environment: PythonEnvironment, + log?: LogOutputChannel, + options?: { showProgress: boolean }, +): Promise { + let data: string; + try { + if (options?.showProgress) { + data = await withProgress( + { + location: ProgressLocation.Notification, + }, + async () => { + return await refreshPipPackagesRaw(environment, log); + }, + ); + } else { + data = await refreshPipPackagesRaw(environment, log); + } + + return parsePipList(data); + } catch (e) { + log?.error('Error refreshing packages', e); + showErrorMessageWithLogs(SysManagerStrings.packageRefreshError, log); + return undefined; + } +} + +export async function refreshPackages( + environment: PythonEnvironment, + api: PythonEnvironmentApi, + manager: PackageManager, +): Promise { + const data = await refreshPipPackages(environment, manager.log); + return (data ?? []).map((pkg) => api.createPackageItem(pkg, environment, manager)); +} + +export async function managePackages( + environment: PythonEnvironment, + options: PackageManagementOptions, + api: PythonEnvironmentApi, + manager: PackageManager, + token?: CancellationToken, +): Promise { + if (environment.version.startsWith('2.')) { + throw new Error('Python 2.* is not supported (deprecated)'); + } + + const useUv = await isUvInstalled(); + const uninstallArgs = ['pip', 'uninstall']; + if (options.uninstall && options.uninstall.length > 0) { + if (useUv) { + await runUV( + [...uninstallArgs, '--python', environment.execInfo.run.executable, ...options.uninstall], + undefined, + manager.log, + token, + ); + } else { + uninstallArgs.push('--yes'); + await runPython( + environment.execInfo.run.executable, + ['-m', ...uninstallArgs, ...options.uninstall], + undefined, + manager.log, + token, + ); + } + } + + const installArgs = ['pip', 'install']; + if (options.upgrade) { + installArgs.push('--upgrade'); + } + if (options.install && options.install.length > 0) { + const processedInstallArgs = processEditableInstallArgs(options.install); + + if (useUv) { + await runUV( + [...installArgs, '--python', environment.execInfo.run.executable, ...processedInstallArgs], + undefined, + manager.log, + token, + ); + } else { + await runPython( + environment.execInfo.run.executable, + ['-m', ...installArgs, ...processedInstallArgs], + undefined, + manager.log, + token, + ); + } + } + + return await refreshPackages(environment, api, manager); +} + +/** + * Process pip install arguments to correctly handle editable installs with extras + * This function will combine consecutive -e arguments that represent the same package with extras + */ +export function processEditableInstallArgs(args: string[]): string[] { + const processedArgs: string[] = []; + let i = 0; + + while (i < args.length) { + if (args[i] === '-e') { + const packagePath = args[i + 1]; + if (!packagePath) { + processedArgs.push(args[i]); + i++; + continue; + } + + if (i + 2 < args.length && args[i + 2] === '-e' && i + 3 < args.length) { + const nextArg = args[i + 3]; + + if (nextArg.startsWith('.[') && nextArg.includes(']')) { + const combinedPath = packagePath + nextArg.substring(1); + processedArgs.push('-e', combinedPath); + i += 4; + continue; + } + } + + processedArgs.push(args[i], packagePath); + i += 2; + } else { + processedArgs.push(args[i]); + i++; + } + } + + return processedArgs; +} + +export async function resolveSystemPythonEnvironmentPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + const resolved = await nativeFinder.resolve(fsPath); + + // This is supposed to handle a python interpreter as long as we know some basic things about it + if (resolved.executable && resolved.version && resolved.prefix) { + const envInfo = getPythonInfo(resolved); + return api.createPythonEnvironmentItem(envInfo, manager); + } +} diff --git a/src/managers/builtin/venvManager.ts b/src/managers/builtin/venvManager.ts new file mode 100644 index 0000000..80bbd63 --- /dev/null +++ b/src/managers/builtin/venvManager.ts @@ -0,0 +1,619 @@ +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { + commands, + EventEmitter, + l10n, + LogOutputChannel, + MarkdownString, + ProgressLocation, + ThemeIcon, + Uri, +} from 'vscode'; +import { + CreateEnvironmentOptions, + CreateEnvironmentScope, + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + PythonEnvironmentApi, + PythonProject, + QuickCreateConfig, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../../api'; +import { PYTHON_EXTENSION_ID } from '../../common/constants'; +import { VenvManagerStrings } from '../../common/localize'; +import { traceError, traceWarn } from '../../common/logging'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { showErrorMessage, withProgress } from '../../common/window.apis'; +import { findParentIfFile } from '../../features/envCommands'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { getLatest, shortVersion, sortEnvironments } from '../common/utils'; +import { + clearVenvCache, + CreateEnvironmentResult, + createPythonVenv, + findVirtualEnvironments, + getDefaultGlobalVenvLocation, + getGlobalVenvLocation, + getVenvForGlobal, + getVenvForWorkspace, + quickCreateVenv, + removeVenv, + resolveVenvPythonEnvironmentPath, + setVenvForGlobal, + setVenvForWorkspace, + setVenvForWorkspaces, +} from './venvUtils'; + +export class VenvManager implements EnvironmentManager { + private collection: PythonEnvironment[] = []; + private readonly fsPathToEnv: Map = new Map(); + private globalEnv: PythonEnvironment | undefined; + private skipWatcherRefresh = false; + + private readonly _onDidChangeEnvironment = new EventEmitter(); + public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; + + private readonly _onDidChangeEnvironments = new EventEmitter(); + public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + + readonly name: string; + readonly displayName: string; + readonly preferredPackageManagerId: string; + readonly description?: string | undefined; + readonly tooltip?: string | MarkdownString | undefined; + readonly iconPath?: IconPath | undefined; + + constructor( + private readonly nativeFinder: NativePythonFinder, + private readonly api: PythonEnvironmentApi, + private readonly baseManager: EnvironmentManager, + public readonly log: LogOutputChannel, + ) { + this.name = 'venv'; + this.displayName = 'venv'; + // Descriptions were a bit too visually noisy + // https://github.com/microsoft/vscode-python-environments/issues/167 + this.description = undefined; + this.tooltip = new MarkdownString(VenvManagerStrings.venvManagerDescription, true); + this.preferredPackageManagerId = 'ms-python.python:pip'; + this.iconPath = new ThemeIcon('python'); + } + + private _initialized: Deferred | undefined; + async initialize(): Promise { + if (this._initialized) { + return this._initialized.promise; + } + + this._initialized = createDeferred(); + + try { + await this.internalRefresh(undefined, false, VenvManagerStrings.venvInitialize); + } finally { + this._initialized.resolve(); + } + } + + /** + * Returns configuration for quick create in the workspace root, undefined if no suitable Python 3 version is found. + */ + quickCreateConfig(): QuickCreateConfig | undefined { + if (!this.globalEnv || !this.globalEnv.version.startsWith('3.')) { + return undefined; + } + return { + description: l10n.t('Create a virtual environment in workspace root'), + detail: l10n.t( + 'Uses Python version {0} and installs workspace dependencies.', + shortVersion(this.globalEnv.version), + ), + }; + } + + async create( + scope: CreateEnvironmentScope, + options: CreateEnvironmentOptions | undefined, + ): Promise { + try { + this.skipWatcherRefresh = true; + let isGlobal = scope === 'global'; + if (Array.isArray(scope) && scope.length > 1) { + isGlobal = true; + } + let uri: Uri | undefined = undefined; + if (isGlobal) { + uri = options?.quickCreate ? await getDefaultGlobalVenvLocation() : await getGlobalVenvLocation(); + } else { + uri = scope instanceof Uri ? scope : (scope as Uri[])[0]; + } + + if (!uri) { + return; + } + + const venvRoot: Uri = Uri.file(await findParentIfFile(uri.fsPath)); + + const globals = await this.baseManager.getEnvironments('global'); + let result: CreateEnvironmentResult | undefined = undefined; + if (options?.quickCreate) { + // error on missing information + if (!this.globalEnv) { + this.log.error('No base python found'); + showErrorMessage(VenvManagerStrings.venvErrorNoBasePython); + throw new Error('No base python found'); + } + if (!this.globalEnv.version.startsWith('3.')) { + this.log.error('Did not find any base python 3.*'); + globals.forEach((e, i) => { + this.log.error(`${i}: ${e.version} : ${e.environmentPath.fsPath}`); + }); + showErrorMessage(VenvManagerStrings.venvErrorNoPython3); + throw new Error('Did not find any base python 3.*'); + } + if (this.globalEnv && this.globalEnv.version.startsWith('3.')) { + // quick create given correct information + result = await quickCreateVenv( + this.nativeFinder, + this.api, + this.log, + this, + this.globalEnv, + venvRoot, + options?.additionalPackages, + ); + } + } else { + // If quickCreate is not set that means the user triggered this method from + // environment manager View, by selecting the venv manager. + result = await createPythonVenv(this.nativeFinder, this.api, this.log, this, globals, venvRoot, { + showQuickAndCustomOptions: options?.quickCreate === undefined, + }); + } + + if (result?.environment) { + const environment = result.environment; + + this.addEnvironment(environment, true); + + // Add .gitignore to the .venv folder + try { + // determine if env path is python binary or environment folder + let envPath = environment.environmentPath.fsPath; + try { + const stat = await fs.stat(envPath); + if (!stat.isDirectory()) { + // If the env path is a file (likely the python binary), use parent-parent as the env path + // following format of .venv/bin/python or .venv\Scripts\python.exe + envPath = Uri.file(path.dirname(path.dirname(envPath))).fsPath; + } + } catch (err) { + // If stat fails, fallback to original envPath + traceWarn( + `Failed to stat environment path: ${envPath}. Error: ${ + err instanceof Error ? err.message : String(err) + }, continuing to attempt to create .gitignore.`, + ); + } + const gitignorePath = path.join(envPath, '.gitignore'); + await fs.writeFile(gitignorePath, '*\n', { flag: 'w' }); + } catch (err) { + traceError( + `Failed to create .gitignore in venv: ${ + err instanceof Error ? err.message : String(err) + }, continuing.`, + ); + } + + // Open the parent folder of the venv in the current window immediately after creation + const envParent = environment.sysPrefix; + try { + await commands.executeCommand('revealInExplorer', Uri.file(envParent)); + } catch (error) { + showErrorMessage( + l10n.t( + 'Failed to reveal venv parent folder in VS Code Explorer: but venv was still created in {0}', + envParent, + ), + ); + traceError( + `Failed to reveal venv parent folder in VS Code Explorer: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + return result?.environment ?? undefined; + } finally { + this.skipWatcherRefresh = false; + } + } + + /** + * Removes the specified Python environment, updates internal collections, and fires change events as needed. + */ + async remove(environment: PythonEnvironment): Promise { + try { + this.skipWatcherRefresh = true; + + const isRemoved = await removeVenv(environment, this.log); + if (!isRemoved) { + return; + } + this.updateCollection(environment); + this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.remove }]); + + const changedUris = this.updateFsPathToEnv(environment); + + for (const uri of changedUris) { + const newEnv = await this.get(uri); + this._onDidChangeEnvironment.fire({ uri, old: environment, new: newEnv }); + } + + if (this.globalEnv?.envId.id === environment.envId.id) { + await this.set(undefined, undefined); + } + } finally { + this.skipWatcherRefresh = false; + } + } + + private updateCollection(environment: PythonEnvironment): void { + this.collection = this.collection.filter( + (e) => e.environmentPath.fsPath !== environment.environmentPath.fsPath, + ); + } + + private updateFsPathToEnv(environment: PythonEnvironment): Uri[] { + const changed: Uri[] = []; + this.fsPathToEnv.forEach((env, uri) => { + if (env.environmentPath.fsPath === environment.environmentPath.fsPath) { + this.fsPathToEnv.delete(uri); + changed.push(Uri.file(uri)); + } + }); + return changed; + } + + async refresh(scope: RefreshEnvironmentsScope): Promise { + return this.internalRefresh(scope, true, VenvManagerStrings.venvRefreshing); + } + + async watcherRefresh(): Promise { + if (this.skipWatcherRefresh) { + return; + } + return this.internalRefresh(undefined, true, VenvManagerStrings.venvRefreshing); + } + + private async internalRefresh( + scope: RefreshEnvironmentsScope, + hardRefresh: boolean, + title: string, + location: ProgressLocation = ProgressLocation.Window, + ): Promise { + await withProgress( + { + location, + title, + }, + async () => { + const discard = this.collection.map((env) => ({ + kind: EnvironmentChangeKind.remove, + environment: env, + })); + + this.collection = await findVirtualEnvironments( + hardRefresh, + this.nativeFinder, + this.api, + this.log, + this, + scope ? [scope] : undefined, + ); + await this.loadEnvMap(); + + const added = this.collection.map((env) => ({ environment: env, kind: EnvironmentChangeKind.add })); + this._onDidChangeEnvironments.fire([...discard, ...added]); + }, + ); + } + + async getEnvironments(scope: GetEnvironmentsScope): Promise { + await this.initialize(); + + if (scope === 'all') { + return Array.from(this.collection); + } + if (!(scope instanceof Uri)) { + return []; + } + + const env = this.fsPathToEnv.get(scope.fsPath); + return env ? [env] : []; + } + + async get(scope: GetEnvironmentScope): Promise { + await this.initialize(); + + if (!scope) { + // `undefined` for venv scenario return the global environment. + return this.globalEnv; + } + + const project = this.api.getPythonProject(scope); + if (!project) { + return this.globalEnv; + } + + let env = this.fsPathToEnv.get(project.uri.fsPath); + if (!env) { + env = this.findEnvironmentByPath(project.uri.fsPath); + } + + return env ?? this.globalEnv; + } + + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + if (scope === undefined) { + const before = this.globalEnv; + this.globalEnv = environment; + await setVenvForGlobal(environment?.environmentPath.fsPath); + await this.resetGlobalEnv(); + if (before?.envId.id !== this.globalEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv }); + } + return; + } + + if (scope instanceof Uri) { + const pw = this.api.getPythonProject(scope); + if (!pw) { + return; + } + + const before = this.fsPathToEnv.get(pw.uri.fsPath); + if (environment) { + this.fsPathToEnv.set(pw.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(pw.uri.fsPath); + } + await setVenvForWorkspace(pw.uri.fsPath, environment?.environmentPath.fsPath); + + if (before?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment }); + } + } + + if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setVenvForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); + } + } + + async resolve(context: ResolveEnvironmentContext): Promise { + if (context instanceof Uri) { + // NOTE: `environmentPath` for envs in `this.collection` for venv always points to the python + // executable in the venv. This is set when we create the PythonEnvironment object. + const found = this.findEnvironmentByPath(context.fsPath); + if (found) { + // If it is in the collection, then it is a venv, and it should already be fully resolved. + return found; + } + } + + const resolved = await resolveVenvPythonEnvironmentPath( + context.fsPath, + this.nativeFinder, + this.api, + this, + this.baseManager, + ); + if (resolved) { + if (resolved.envId.managerId === `${PYTHON_EXTENSION_ID}:venv`) { + // This is just like finding a new environment or creating a new one. + // Add it to collection, and trigger the added event. + this.addEnvironment(resolved, true); + + // We should only return the resolved env if it is a venv. + // Fall through an return undefined if it is not a venv + return resolved; + } + } + + return undefined; + } + + async clearCache(): Promise { + await clearVenvCache(); + } + + private addEnvironment(environment: PythonEnvironment, raiseEvent?: boolean): void { + if (this.collection.find((e) => e.envId.id === environment.envId.id)) { + return; + } + + const oldEnv = this.findEnvironmentByPath(environment.environmentPath.fsPath); + if (oldEnv) { + this.collection = this.collection.filter((e) => e.envId.id !== oldEnv.envId.id); + this.collection.push(environment); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([ + { environment: oldEnv, kind: EnvironmentChangeKind.remove }, + { environment, kind: EnvironmentChangeKind.add }, + ]); + } + } else { + this.collection.push(environment); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.add }]); + } + } + } + + private async resetGlobalEnv() { + this.globalEnv = undefined; + const globals = await this.baseManager.getEnvironments('global'); + await this.loadGlobalEnv(globals); + } + + /** + * Loads and sets the global Python environment from the provided list, resolving if necessary. O(g) where g = globals.length + */ + private async loadGlobalEnv(globals: PythonEnvironment[]) { + this.globalEnv = undefined; + + // Try to find a global environment + const fsPath = await getVenvForGlobal(); + + if (fsPath) { + this.globalEnv = this.findEnvironmentByPath(fsPath) ?? this.findEnvironmentByPath(fsPath, globals); + + // If the environment is not found, resolve the fsPath. Could be portable conda. + if (!this.globalEnv) { + this.globalEnv = await resolveVenvPythonEnvironmentPath( + fsPath, + this.nativeFinder, + this.api, + this, + this.baseManager, + ); + + // If the environment is resolved, add it to the collection + if (this.globalEnv) { + this.addEnvironment(this.globalEnv, false); + } + } + } + + // If a global environment is still not set, use latest from globals + if (!this.globalEnv) { + this.globalEnv = getLatest(globals); + } + } + + /** + * Loads and maps Python environments to their corresponding project paths in the workspace. about O(p × e) where p = projects.len and e = environments.len + */ + private async loadEnvMap() { + const globals = await this.baseManager.getEnvironments('global'); + await this.loadGlobalEnv(globals); + + this.fsPathToEnv.clear(); + + const sorted = sortEnvironments(this.collection); + const projectPaths = this.api.getPythonProjects().map((p) => path.normalize(p.uri.fsPath)); + const events: (() => void)[] = []; + // Iterates through all workspace projects + for (const p of projectPaths) { + const env = await getVenvForWorkspace(p); + if (env) { + // from env path find PythonEnvironment object in the collection. + let foundEnv = this.findEnvironmentByPath(env, sorted) ?? this.findEnvironmentByPath(env, globals); + const previousEnv = this.fsPathToEnv.get(p); + const pw = this.api.getPythonProject(Uri.file(p)); + if (!foundEnv) { + // attempt to resolve + const resolved = await resolveVenvPythonEnvironmentPath( + env, + this.nativeFinder, + this.api, + this, + this.baseManager, + ); + if (resolved) { + // If resolved; add it to the venvManager collection + this.addEnvironment(resolved, false); + foundEnv = resolved; + } else { + this.log.error(`Failed to resolve python environment: ${env}`); + return; + } + } + // Given found env, add it to the map and fire the event if needed. + this.fsPathToEnv.set(p, foundEnv); + if (pw && previousEnv?.envId.id !== foundEnv.envId.id) { + events.push(() => + this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: foundEnv }), + ); + } + } else { + // Search through all known environments (e) and check if any are associated with the current project path. If so, add that environment and path in the map. + const found = sorted.find((e) => { + const t = this.api.getPythonProject(e.environmentPath)?.uri.fsPath; + return t && path.normalize(t) === p; + }); + if (found) { + this.fsPathToEnv.set(p, found); + } + } + } + + events.forEach((e) => e()); + } + + /** + * Finds a PythonEnvironment in the given collection (or all environments) that matches the provided file system path. O(e) where e = environments.len + */ + private findEnvironmentByPath(fsPath: string, collection?: PythonEnvironment[]): PythonEnvironment | undefined { + const normalized = path.normalize(fsPath); + const envs = collection ?? this.collection; + return envs.find((e) => { + const n = path.normalize(e.environmentPath.fsPath); + return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized; + }); + } + + /** + * Returns all Python projects associated with the given environment. + * O(p), where p is project.len + */ + public getProjectsByEnvironment(environment: PythonEnvironment): PythonProject[] { + const projects: PythonProject[] = []; + this.fsPathToEnv.forEach((env, fsPath) => { + if (env.envId.id === environment.envId.id) { + const p = this.api.getPythonProject(Uri.file(fsPath)); + if (p) { + projects.push(p); + } + } + }); + return projects; + } +} diff --git a/src/managers/builtin/venvStepBasedFlow.ts b/src/managers/builtin/venvStepBasedFlow.ts new file mode 100644 index 0000000..b679cd6 --- /dev/null +++ b/src/managers/builtin/venvStepBasedFlow.ts @@ -0,0 +1,400 @@ +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { LogOutputChannel, QuickInputButtons, Uri } from 'vscode'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonProject } from '../../api'; +import { Pickers, VenvManagerStrings } from '../../common/localize'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; +import { showInputBoxWithButtons, showQuickPickWithButtons } from '../../common/window.apis'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { getWorkspacePackagesToInstall, PipPackages } from './pipUtils'; +import { CreateEnvironmentResult, createWithProgress, ensureGlobalEnv } from './venvUtils'; + +/** + * State interface for the venv creation flow. + * + * This keeps track of all user selections throughout the flow, + * allowing the wizard to maintain context when navigating backwards. + * Each property represents a piece of data collected during a step in the workflow. + */ +interface VenvCreationState { + // Base Python environment to use for creating the venv + basePython?: PythonEnvironment; + + // Whether to use quick create or custom create + isQuickCreate?: boolean; + + // Name for the venv + venvName?: string; + + // Packages to install in the venv + // undefined = not yet set, null = user canceled during package selection + packages?: PipPackages | null; + + // Store the sorted environments to avoid re-sorting when navigating back + sortedEnvs?: PythonEnvironment[]; + + // Tracks whether user completed the package selection step + // undefined = not yet reached, true = completed, false = canceled + packageSelectionCompleted?: boolean; + + // References to API and project needed for package selection + api?: PythonEnvironmentApi; + project?: PythonProject[]; + + // Root directory where venv will be created (used for path validation) + venvRoot?: Uri; +} +/** + * Type definition for step functions in the wizard-like flow. + * + * Each step function: + * 1. Takes the current state as input + * 2. Interacts with the user through UI + * 3. Updates the state with new data + * 4. Returns the next step function to execute or null if flow is complete + * + * This pattern enables proper back navigation between steps without losing context. + */ +type StepFunction = (state: VenvCreationState) => Promise; + +/** + * Step 1: Select quick create or custom create + */ +async function selectCreateType(state: VenvCreationState): Promise { + try { + if (!state.sortedEnvs || state.sortedEnvs.length === 0) { + return null; + } + + // Show the quick/custom selection dialog with descriptive options + const selection = await showQuickPickWithButtons( + [ + { + label: VenvManagerStrings.quickCreate, + description: VenvManagerStrings.quickCreateDescription, + detail: `Uses Python version ${state.sortedEnvs[0].version} and installs workspace dependencies.`, + }, + { + label: VenvManagerStrings.customize, + description: VenvManagerStrings.customizeDescription, + }, + ], + { + placeHolder: VenvManagerStrings.selectQuickOrCustomize, + ignoreFocusOut: true, + showBackButton: true, + }, + ); + + // Handle cancellation - return null to exit the flow + if (!selection || Array.isArray(selection)) { + return null; // Exit the flow without creating an environment + } + + if (selection.label === VenvManagerStrings.quickCreate) { + // For quick create, use the first Python environment and proceed to completion + state.isQuickCreate = true; + state.basePython = state.sortedEnvs[0]; + // Quick create is complete - no more steps needed + return null; + } else { + // For custom create, move to Python selection step + state.isQuickCreate = false; + // Next step: select base Python version + return selectBasePython; + } + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // This is the first step, so return null to exit the flow + return null; + } + throw ex; + } +} + +/** + * Step 2: Select base Python interpreter to use for venv creation + */ +async function selectBasePython(state: VenvCreationState): Promise { + try { + if (!state.sortedEnvs || state.sortedEnvs.length === 0) { + return null; + } + + // Create items for each available Python environment with descriptive labels + const items = state.sortedEnvs.map((e) => { + const pathDescription = e.displayPath; + const description = + e.description && e.description.trim() ? `${e.description} (${pathDescription})` : pathDescription; + + return { + label: e.displayName ?? e.name, + description: description, + e: e, + }; + }); + + // Show Python environment selection dialog with back button + const selected = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Environments.selectEnvironment, + ignoreFocusOut: true, + showBackButton: true, + }); + + // Handle cancellation (Escape key or dialog close) + if (!selected || Array.isArray(selected)) { + return null; // Exit the flow without creating an environment + } + + // Update state with selected Python environment + const basePython = (selected as { e: PythonEnvironment }).e; + if (!basePython || !basePython.execInfo) { + // Invalid selection + return null; + } + + state.basePython = basePython; + + // Next step: input venv name + return enterEnvironmentName; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to create type selection if we came from there + if (state.isQuickCreate !== undefined) { + return selectCreateType; + } + return null; + } + throw ex; + } +} + +/** + * Step 3: Enter environment name + */ +async function enterEnvironmentName(state: VenvCreationState): Promise { + try { + // Show input box for venv name with back button + const name = await showInputBoxWithButtons({ + prompt: VenvManagerStrings.venvName, + value: '.venv', // Default name + ignoreFocusOut: true, + showBackButton: true, + validateInput: async (value) => { + if (!value) { + return VenvManagerStrings.venvNameErrorEmpty; + } + + // Validate that the path doesn't already exist + if (state.venvRoot) { + try { + const fullPath = path.join(state.venvRoot.fsPath, value); + if (await fse.pathExists(fullPath)) { + return VenvManagerStrings.venvNameErrorExists; + } + } catch (_) { + // Ignore file system errors during validation + } + } + return null; + }, + }); + + // Handle cancellation (Escape key or dialog close) + if (!name) { + return null; // Exit the flow without creating an environment + } + + state.venvName = name; + + // Next step: select packages + return selectPackages; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to base Python selection + return selectBasePython; + } + throw ex; + } +} + +/** + * Step 4: Select packages to install + */ +async function selectPackages(state: VenvCreationState): Promise { + try { + // Show package selection UI using existing function from pipUtils + + // Create packages structure with empty array and showing the skip option + const packagesOptions = { + showSkipOption: true, + install: [], + }; + + // Use existing getWorkspacePackagesToInstall that will show the UI with all options + // The function already handles showing workspace deps, PyPI options, and skip + if (state.api) { + const result = await getWorkspacePackagesToInstall( + state.api, + packagesOptions, + state.project, // Use project from state if available + undefined, // No environment yet since we're creating it + ); + + if (result !== undefined) { + // User made a selection or clicked Skip + state.packageSelectionCompleted = true; + state.packages = result; + } else { + // User pressed Escape or closed the dialog + state.packageSelectionCompleted = false; + state.packages = null; // Explicitly mark as canceled + } + } else { + // No API, can't show package selection + state.packageSelectionCompleted = true; + state.packages = { + install: [], + uninstall: [], + }; + } + + // Final step - no more steps after this + return null; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to environment name input + return enterEnvironmentName; + } + throw ex; + } +} + +/** + * Main entry point for the step-based venv creation flow. + * + * This function implements a step-based wizard pattern for creating Python virtual + * environments. The user can navigate through steps and also cancel at any point + * by pressing Escape or closing any dialog. + * + * @param nativeFinder Python finder for resolving Python paths + * @param api Python Environment API + * @param log Logger for recording operations + * @param manager Environment manager + * @param basePythons Available Python environments + * @param venvRoot Root directory where the venv will be created + * @param options Configuration options + * @returns The result of environment creation or undefined if cancelled at any point + */ +export async function createStepBasedVenvFlow( + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + basePythons: PythonEnvironment[], + venvRoot: Uri, + options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] }, +): Promise { + // Sort and filter available Python environments + const sortedEnvs = ensureGlobalEnv(basePythons, log); + if (sortedEnvs.length === 0) { + return { + envCreationErr: 'No suitable Python environments found', + }; + } + + // Initialize the state object that will track user selections + const state: VenvCreationState = { + sortedEnvs, // Store sorted environments in state to avoid re-sorting + api, // Store API reference for package selection + project: [api.getPythonProject(venvRoot)].filter(Boolean) as PythonProject[], // Get project for venvRoot + venvRoot, // Store venvRoot for path validation + }; + + try { + // Determine the first step based on options + let currentStep: StepFunction | null = options.showQuickAndCustomOptions ? selectCreateType : selectBasePython; + + // Execute steps until completion or cancellation + // When a step returns null, it means either: + // 1. The step has completed successfully and there are no more steps + // 2. The user cancelled the step (pressed Escape or closed the dialog) + while (currentStep !== null) { + currentStep = await currentStep(state); + } + + // After workflow completes, check if we have all required data + + // Case 1: Quick create flow + if (state.isQuickCreate && state.basePython) { + // Use quick create flow + sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' }); + // Use the default .venv name for quick create + const quickEnvPath = path.join(venvRoot.fsPath, '.venv'); + return await createWithProgress(nativeFinder, api, log, manager, state.basePython, venvRoot, quickEnvPath, { + install: options.additionalPackages || [], + uninstall: [], + }); + } + // Case 2: Custom create flow + // Note: requires selectPackage step completed + else if ( + !state.isQuickCreate && + state.basePython && + state.venvName && + // The user went through all steps without cancellation + // (specifically checking that package selection wasn't canceled) + state.packageSelectionCompleted !== false + ) { + sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'custom' }); + + const project = api.getPythonProject(venvRoot); + const envPath = path.join(venvRoot.fsPath, state.venvName); + + // Get packages to install - if the selectPackages step was completed, state.packages might already be set + // If not, we'll fetch packages here to ensure proper package detection + let packages = state.packages; + if (!packages) { + packages = await getWorkspacePackagesToInstall( + api, + { showSkipOption: true, install: [] }, + project ? [project] : undefined, + undefined, + log, + ); + } + + // Combine packages from multiple sources + const allPackages: string[] = []; + + // 1. User-selected packages from workspace dependencies or PyPI during the wizard flow + // (may be undefined if user skipped package selection or canceled) + if (packages?.install) { + allPackages.push(...packages.install); + } + + // 2. Additional packages provided by the caller of createStepBasedVenvFlow + // (e.g., packages required by the extension itself) + if (options.additionalPackages) { + allPackages.push(...options.additionalPackages); + } + + return await createWithProgress(nativeFinder, api, log, manager, state.basePython, venvRoot, envPath, { + install: allPackages, + uninstall: [], + }); + } + + // If we get here, the flow was cancelled (e.g., user pressed Escape) + // Return undefined to indicate no environment was created + return undefined; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // This should not happen as back navigation is handled within each step + // But if it does, restart the flow + return await createStepBasedVenvFlow(nativeFinder, api, log, manager, basePythons, venvRoot, options); + } + throw ex; // Re-throw other errors + } +} diff --git a/src/managers/builtin/venvUtils.ts b/src/managers/builtin/venvUtils.ts new file mode 100644 index 0000000..b294319 --- /dev/null +++ b/src/managers/builtin/venvUtils.ts @@ -0,0 +1,468 @@ +import * as fsapi from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { l10n, LogOutputChannel, ProgressLocation, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { Common, VenvManagerStrings } from '../../common/localize'; +import { traceInfo } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { EventNames } from '../../common/telemetry/constants'; +import { sendTelemetryEvent } from '../../common/telemetry/sender'; +import { normalizePath } from '../../common/utils/pathUtils'; +import { + showErrorMessage, + showOpenDialog, + showQuickPick, + showWarningMessage, + withProgress, +} from '../../common/window.apis'; +import { getConfiguration } from '../../common/workspace.apis'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils'; +import { isUvInstalled, runPython, runUV } from './helpers'; +import { getProjectInstallable, PipPackages } from './pipUtils'; +import { resolveSystemPythonEnvironmentPath } from './utils'; +import { createStepBasedVenvFlow } from './venvStepBasedFlow'; + +export const VENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:venv:WORKSPACE_SELECTED`; +export const VENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:venv:GLOBAL_SELECTED`; + +/** + * Result of environment creation operation. + */ +export interface CreateEnvironmentResult { + /** + * The created environment, if successful. + */ + environment?: PythonEnvironment; + + /* + * Exists if error occurred during environment creation and includes error explanation. + */ + envCreationErr?: string; + + /* + * Exists if error occurred while installing packages and includes error description. + */ + pkgInstallationErr?: string; +} + +export async function clearVenvCache(): Promise { + const keys = [VENV_WORKSPACE_KEY, VENV_GLOBAL_KEY]; + const state = await getWorkspacePersistentState(); + await state.clear(keys); +} + +export async function getVenvForWorkspace(fsPath: string): Promise { + if (process.env.VIRTUAL_ENV) { + return process.env.VIRTUAL_ENV; + } + + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(VENV_WORKSPACE_KEY); + if (data) { + try { + const envPath = data[fsPath]; + if (await fsapi.pathExists(envPath)) { + return envPath; + } + setVenvForWorkspace(fsPath, undefined); + } catch { + return undefined; + } + } + return undefined; +} + +export async function setVenvForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(VENV_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(VENV_WORKSPACE_KEY, data); +} + +export async function setVenvForWorkspaces(fsPaths: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(VENV_WORKSPACE_KEY)) ?? {}; + fsPaths.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(VENV_WORKSPACE_KEY, data); +} + +export async function getVenvForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + const envPath: string | undefined = await state.get(VENV_GLOBAL_KEY); + if (envPath && (await fsapi.pathExists(envPath))) { + return envPath; + } + return undefined; +} + +export async function setVenvForGlobal(envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(VENV_GLOBAL_KEY, envPath); +} + +function getName(binPath: string): string { + const dir1 = path.dirname(binPath); + if (dir1.endsWith('bin') || dir1.endsWith('Scripts') || dir1.endsWith('scripts')) { + return path.basename(path.dirname(dir1)); + } + return path.basename(dir1); +} + +async function getPythonInfo(env: NativeEnvInfo): Promise { + if (env.executable && env.version && env.prefix) { + const venvName = env.name ?? getName(env.executable); + const sv = shortVersion(env.version); + const name = `${venvName} (${sv})`; + + const binDir = path.dirname(env.executable); + + const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir); + + return { + name: name, + displayName: name, + shortDisplayName: `${sv} (${venvName})`, + displayPath: env.executable, + version: env.version, + description: undefined, + tooltip: env.executable, + environmentPath: Uri.file(env.executable), + iconPath: new ThemeIcon('python'), + sysPrefix: env.prefix, + execInfo: { + run: { + executable: env.executable, + }, + activatedRun: { + executable: env.executable, + }, + shellActivation, + shellDeactivation, + }, + }; + } else { + throw new Error(`Invalid python info: ${JSON.stringify(env)}`); + } +} + +export async function findVirtualEnvironments( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + uris?: Uri[], +): Promise { + const collection: PythonEnvironment[] = []; + const data = await nativeFinder.refresh(hardRefresh, uris); + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter((e) => e.kind === NativePythonEnvironmentKind.venv); + + for (const e of envs) { + if (!(e.prefix && e.executable && e.version)) { + log.warn(`Invalid venv environment: ${JSON.stringify(e)}`); + continue; + } + + const env = api.createPythonEnvironmentItem(await getPythonInfo(e), manager); + collection.push(env); + log.info(`Found venv environment: ${env.name}`); + } + return collection; +} + +export async function getDefaultGlobalVenvLocation(): Promise { + const dir = path.join(os.homedir(), '.virtualenvs'); + await fsapi.ensureDir(dir); + return Uri.file(dir); +} + +function getVenvFoldersSetting(): string[] { + const settings = getConfiguration('python'); + return settings.get('venvFolders', []); +} + +interface FolderQuickPickItem extends QuickPickItem { + uri?: Uri; +} +export async function getGlobalVenvLocation(): Promise { + const items: FolderQuickPickItem[] = [ + { + label: Common.browse, + description: VenvManagerStrings.venvGlobalFolder, + }, + ]; + + const venvPaths = getVenvFoldersSetting(); + if (venvPaths.length > 0) { + items.push( + { + label: VenvManagerStrings.venvGlobalFoldersSetting, + kind: QuickPickItemKind.Separator, + }, + ...venvPaths.map((p) => ({ + label: path.basename(p), + description: path.resolve(p), + uri: Uri.file(path.resolve(p)), + })), + ); + } + + if (process.env.WORKON_HOME) { + items.push( + { + label: 'virtualenvwrapper', + kind: QuickPickItemKind.Separator, + }, + { + label: 'WORKON_HOME (env variable)', + description: process.env.WORKON_HOME, + uri: Uri.file(process.env.WORKON_HOME), + }, + ); + } + + const selected = await showQuickPick(items, { + placeHolder: VenvManagerStrings.venvGlobalFolder, + ignoreFocusOut: true, + }); + + if (selected) { + if (selected.label === Common.browse) { + const result = await showOpenDialog({ + canSelectFiles: false, + canSelectFolders: true, + canSelectMany: false, + openLabel: Common.selectFolder, + }); + if (result && result.length > 0) { + return result[0]; + } + } else if (selected.uri) { + return selected.uri; + } + } + return undefined; +} + +export async function createWithProgress( + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + basePython: PythonEnvironment, + venvRoot: Uri, + envPath: string, + packages?: PipPackages, +): Promise { + const pythonPath = + os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python'); + + return await withProgress( + { + location: ProgressLocation.Notification, + title: l10n.t( + 'Creating virtual environment named {0} using python version {1}.', + path.basename(envPath), + basePython.version, + ), + }, + async () => { + const result: CreateEnvironmentResult = {}; + try { + const useUv = await isUvInstalled(log); + // env creation + if (basePython.execInfo?.run.executable) { + if (useUv) { + await runUV( + ['venv', '--verbose', '--seed', '--python', basePython.execInfo?.run.executable, envPath], + venvRoot.fsPath, + log, + ); + } else { + await runPython( + basePython.execInfo.run.executable, + ['-m', 'venv', envPath], + venvRoot.fsPath, + manager.log, + ); + } + if (!(await fsapi.pathExists(pythonPath))) { + throw new Error('no python executable found in virtual environment'); + } + } + + // handle admin of new env + const resolved = await nativeFinder.resolve(pythonPath); + const env = api.createPythonEnvironmentItem(await getPythonInfo(resolved), manager); + + // install packages + if (packages && (packages.install.length > 0 || packages.uninstall.length > 0)) { + try { + await api.managePackages(env, { + upgrade: false, + install: packages?.install, + uninstall: packages?.uninstall ?? [], + }); + } catch (e) { + // error occurred while installing packages + result.pkgInstallationErr = e instanceof Error ? e.message : String(e); + } + } + result.environment = env; + } catch (e) { + log.error(`Failed to create virtual environment: ${e}`); + result.envCreationErr = `Failed to create virtual environment: ${e}`; + } + return result; + }, + ); +} + +export function ensureGlobalEnv(basePythons: PythonEnvironment[], log: LogOutputChannel): PythonEnvironment[] { + if (basePythons.length === 0) { + log.error('No base python found'); + showErrorMessage(VenvManagerStrings.venvErrorNoBasePython); + throw new Error('No base python found'); + } + + const filtered = basePythons.filter((e) => e.version.startsWith('3.')); + if (filtered.length === 0) { + log.error('Did not find any base python 3.*'); + showErrorMessage(VenvManagerStrings.venvErrorNoPython3); + basePythons.forEach((e, i) => { + log.error(`${i}: ${e.version} : ${e.environmentPath.fsPath}`); + }); + throw new Error('Did not find any base python 3.*'); + } + + return sortEnvironments(filtered); +} + +export async function quickCreateVenv( + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + baseEnv: PythonEnvironment, + venvRoot: Uri, + additionalPackages?: string[], +): Promise { + const project = api.getPythonProject(venvRoot); + + sendTelemetryEvent(EventNames.VENV_CREATION, undefined, { creationType: 'quick' }); + const installables = await getProjectInstallable(api, project ? [project] : undefined); + const allPackages = []; + allPackages.push(...(installables?.flatMap((i) => i.args ?? []) ?? [])); + if (additionalPackages) { + allPackages.push(...additionalPackages); + } + + // Check if .venv already exists + let venvPath = path.join(venvRoot.fsPath, '.venv'); + if (await fsapi.pathExists(venvPath)) { + // increment to create a unique name, e.g. .venv-1 + let i = 1; + while (await fsapi.pathExists(`${venvPath}-${i}`)) { + i++; + } + venvPath = `${venvPath}-${i}`; + } + + // createWithProgress handles building CreateEnvironmentResult and adding err msgs + return await createWithProgress(nativeFinder, api, log, manager, baseEnv, venvRoot, venvPath, { + install: allPackages, + uninstall: [], + }); +} + +export async function createPythonVenv( + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + basePythons: PythonEnvironment[], + venvRoot: Uri, + options: { showQuickAndCustomOptions: boolean; additionalPackages?: string[] }, +): Promise { + return createStepBasedVenvFlow(nativeFinder, api, log, manager, basePythons, venvRoot, options); +} + +export async function removeVenv(environment: PythonEnvironment, log: LogOutputChannel): Promise { + const pythonPath = os.platform() === 'win32' ? 'python.exe' : 'python'; + + const envPath = environment.environmentPath.fsPath.endsWith(pythonPath) + ? path.dirname(path.dirname(environment.environmentPath.fsPath)) + : environment.environmentPath.fsPath; + + // Normalize path for UI display - ensure forward slashes on Windows + const displayPath = normalizePath(envPath); + + const confirm = await showWarningMessage( + l10n.t('Are you sure you want to remove {0}?', displayPath), + { + modal: true, + }, + { title: Common.yes }, + { title: Common.no, isCloseAffordance: true }, + ); + if (confirm?.title === Common.yes) { + const result = await withProgress( + { + location: ProgressLocation.Notification, + title: VenvManagerStrings.venvRemoving, + }, + async () => { + try { + await fsapi.remove(envPath); + return true; + } catch (e) { + log.error(`Failed to remove virtual environment: ${e}`); + showErrorMessage(VenvManagerStrings.venvRemoveFailed); + return false; + } + }, + ); + return result; + } + + traceInfo(`User cancelled removal of virtual environment: ${displayPath}`); + return false; +} + +export async function resolveVenvPythonEnvironmentPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, + baseManager: EnvironmentManager, +): Promise { + const resolved = await nativeFinder.resolve(fsPath); + + if (resolved.kind === NativePythonEnvironmentKind.venv) { + const envInfo = await getPythonInfo(resolved); + return api.createPythonEnvironmentItem(envInfo, manager); + } + + return resolveSystemPythonEnvironmentPath(fsPath, nativeFinder, api, baseManager); +} diff --git a/src/managers/common/nativePythonFinder.ts b/src/managers/common/nativePythonFinder.ts index bf4bfe2..b84c0a9 100644 --- a/src/managers/common/nativePythonFinder.ts +++ b/src/managers/common/nativePythonFinder.ts @@ -1,29 +1,34 @@ +import * as ch from 'child_process'; import * as fs from 'fs-extra'; import * as path from 'path'; -import * as rpc from 'vscode-jsonrpc/node'; -import * as ch from 'child_process'; -import { PYTHON_EXTENSION_ID } from '../../common/constants'; -import { getExtension } from '../../common/extension.apis'; -import { getUserHomeDir, isWindows, noop, untildify } from './utils'; -import { Disposable, ExtensionContext, LogOutputChannel, Uri } from 'vscode'; import { PassThrough } from 'stream'; +import { Disposable, ExtensionContext, LogOutputChannel, Uri } from 'vscode'; +import * as rpc from 'vscode-jsonrpc/node'; import { PythonProjectApi } from '../../api'; -import { getConfiguration } from '../../common/workspace.apis'; +import { ENVS_EXTENSION_ID, PYTHON_EXTENSION_ID } from '../../common/constants'; +import { getExtension } from '../../common/extension.apis'; +import { traceError, traceLog, traceVerbose, traceWarn } from '../../common/logging'; +import { untildify, untildifyArray } from '../../common/utils/pathUtils'; +import { isWindows } from '../../common/utils/platformUtils'; import { createRunningWorkerPool, WorkerPool } from '../../common/utils/workerPool'; -import { traceVerbose } from '../../common/logging'; - -let PYTHON_EXTENSION_ROOT_DIR: string | undefined; -function getNativePythonToolsPath(): string { - if (!PYTHON_EXTENSION_ROOT_DIR) { - const python = getExtension(PYTHON_EXTENSION_ID); - PYTHON_EXTENSION_ROOT_DIR = python?.extensionPath; +import { getConfiguration, getWorkspaceFolders } from '../../common/workspace.apis'; +import { noop } from './utils'; + +export async function getNativePythonToolsPath(): Promise { + const envsExt = getExtension(ENVS_EXTENSION_ID); + if (envsExt) { + const petPath = path.join(envsExt.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'); + if (await fs.pathExists(petPath)) { + return petPath; + } } - if (!PYTHON_EXTENSION_ROOT_DIR) { + + const python = getExtension(PYTHON_EXTENSION_ID); + if (!python) { throw new Error('Python extension not found'); } - return isWindows() - ? path.join(PYTHON_EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet.exe') - : path.join(PYTHON_EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet'); + + return path.join(python.extensionPath, 'python-env-tools', 'bin', isWindows() ? 'pet.exe' : 'pet'); } export interface NativeEnvInfo { @@ -179,19 +184,25 @@ class NativePythonFinderImpl implements NativePythonFinder { } private getRefreshOptions(options?: NativePythonEnvironmentKind | Uri[]): RefreshOptions | undefined { + // settings on where else to search + const venvFolders = getPythonSettingAndUntildify('venvFolders') ?? []; if (options) { if (typeof options === 'string') { + // kind return { searchKind: options }; } if (Array.isArray(options)) { - return { searchPaths: options.map((item) => item.fsPath) }; + const uriSearchPaths = options.map((item) => item.fsPath); + uriSearchPaths.push(...venvFolders); + return { searchPaths: uriSearchPaths }; } } + // return undefined to use configured defaults (for nativeFinder refresh) return undefined; } private start(): rpc.MessageConnection { - this.outputChannel.info(`Starting Python Locator ${this.toolPath} server`); + this.outputChannel.info(`[pet] Starting Python Locator ${this.toolPath} server`); // jsonrpc package cannot handle messages coming through too quickly. // Lets handle the messages and close the stream only when @@ -202,7 +213,7 @@ class NativePythonFinderImpl implements NativePythonFinder { try { const proc = ch.spawn(this.toolPath, ['server'], { env: process.env }); proc.stdout.pipe(readable, { end: false }); - proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + proc.stderr.on('data', (data) => this.outputChannel.error(`[pet] ${data.toString()}`)); writable.pipe(proc.stdin, { end: false }); disposables.push({ @@ -212,12 +223,12 @@ class NativePythonFinderImpl implements NativePythonFinder { proc.kill(); } } catch (ex) { - this.outputChannel.error('Error disposing finder', ex); + this.outputChannel.error('[pet] Error disposing finder', ex); } }, }); } catch (ex) { - this.outputChannel.error(`Error starting Python Finder ${this.toolPath} server`, ex); + this.outputChannel.error(`[pet] Error starting Python Finder ${this.toolPath} server`, ex); } const connection = rpc.createMessageConnection( new rpc.StreamMessageReader(readable), @@ -230,27 +241,28 @@ class NativePythonFinderImpl implements NativePythonFinder { writable.end(); }), connection.onError((ex) => { - this.outputChannel.error('Connection Error:', ex); + this.outputChannel.error('[pet] Connection Error:', ex); }), connection.onNotification('log', (data: NativeLog) => { + const msg = `[pet] ${data.message}`; switch (data.level) { case 'info': - this.outputChannel.info(data.message); + this.outputChannel.info(msg); break; case 'warning': - this.outputChannel.warn(data.message); + this.outputChannel.warn(msg); break; case 'error': - this.outputChannel.error(data.message); + this.outputChannel.error(msg); break; case 'debug': - this.outputChannel.debug(data.message); + this.outputChannel.debug(msg); break; default: - this.outputChannel.trace(data.message); + this.outputChannel.trace(msg); } }), - connection.onNotification('telemetry', (data) => this.outputChannel.info(`Telemetry: `, data)), + connection.onNotification('telemetry', (data) => this.outputChannel.info('[pet] Telemetry: ', data)), connection.onClose(() => { disposables.forEach((d) => d.dispose()); }), @@ -277,7 +289,9 @@ class NativePythonFinderImpl implements NativePythonFinder { executable: data.executable, }) .then((environment: NativeEnvInfo) => { - this.outputChannel.info(`Resolved ${environment.executable}`); + this.outputChannel.info( + `Resolved environment during PET refresh: ${environment.executable}`, + ); nativeInfo.push(environment); }) .catch((ex) => @@ -296,7 +310,7 @@ class NativePythonFinderImpl implements NativePythonFinder { await this.connection.sendRequest<{ duration: number }>('refresh', refreshOptions); await Promise.all(unresolved); } catch (ex) { - this.outputChannel.error('Error refreshing', ex); + this.outputChannel.error('[pet] Error refreshing', ex); throw ex; } finally { disposables.forEach((d) => d.dispose()); @@ -312,23 +326,27 @@ class NativePythonFinderImpl implements NativePythonFinder { * Must be invoked when ever there are changes to any data related to the configuration details. */ private async configure() { + // Get all extra search paths including legacy settings and new searchPaths + const extraSearchPaths = await getAllExtraSearchPaths(); + const options: ConfigurationOptions = { workspaceDirectories: this.api.getPythonProjects().map((item) => item.uri.fsPath), - // We do not want to mix this with `search_paths` - environmentDirectories: getCustomVirtualEnvDirs(), + environmentDirectories: extraSearchPaths, condaExecutable: getPythonSettingAndUntildify('condaPath'), poetryExecutable: getPythonSettingAndUntildify('poetryPath'), cacheDirectory: this.cacheDirectory?.fsPath, }; // No need to send a configuration request, is there are no changes. if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + this.outputChannel.debug('[pet] configure: No changes detected, skipping configuration update.'); return; } + this.outputChannel.info('[pet] configure: Sending configuration update:', JSON.stringify(options)); try { this.lastConfiguration = options; await this.connection.sendRequest('configure', options); } catch (ex) { - this.outputChannel.error('Configuration error', ex); + this.outputChannel.error('[pet] configure: Configuration error', ex); } } } @@ -341,19 +359,18 @@ type ConfigurationOptions = { cacheDirectory?: string; }; /** - * Gets all custom virtual environment locations to look for environments. + * Gets all custom virtual environment locations to look for environments from the legacy python settings (venvPath, venvFolders). */ -function getCustomVirtualEnvDirs(): string[] { +function getCustomVirtualEnvDirsLegacy(): string[] { const venvDirs: string[] = []; const venvPath = getPythonSettingAndUntildify('venvPath'); if (venvPath) { venvDirs.push(untildify(venvPath)); } const venvFolders = getPythonSettingAndUntildify('venvFolders') ?? []; - const homeDir = getUserHomeDir(); - if (homeDir) { - venvFolders.map((item) => path.join(homeDir, item)).forEach((d) => venvDirs.push(d)); - } + venvFolders.forEach((item) => { + venvDirs.push(item); + }); return Array.from(new Set(venvDirs)); } @@ -365,6 +382,109 @@ function getPythonSettingAndUntildify(name: string, scope?: Uri): T | undefin return value; } +/** + * Gets all extra environment search paths from various configuration sources. + * Combines legacy python settings (with migration), globalSearchPaths, and workspaceSearchPaths. + * @returns Array of search directory paths + */ +export async function getAllExtraSearchPaths(): Promise { + const searchDirectories: string[] = []; + + // add legacy custom venv directories + const customVenvDirs = getCustomVirtualEnvDirsLegacy(); + searchDirectories.push(...customVenvDirs); + + // Get globalSearchPaths + const globalSearchPaths = getGlobalSearchPaths().filter((path) => path && path.trim() !== ''); + searchDirectories.push(...globalSearchPaths); + + // Get workspaceSearchPaths + const workspaceSearchPaths = getWorkspaceSearchPaths(); + + // Resolve relative paths against workspace folders + for (const searchPath of workspaceSearchPaths) { + if (!searchPath || searchPath.trim() === '') { + continue; + } + + const trimmedPath = searchPath.trim(); + + if (path.isAbsolute(trimmedPath)) { + // Absolute path - use as is + searchDirectories.push(trimmedPath); + } else { + // Relative path - resolve against all workspace folders + const workspaceFolders = getWorkspaceFolders(); + if (workspaceFolders) { + for (const workspaceFolder of workspaceFolders) { + const resolvedPath = path.resolve(workspaceFolder.uri.fsPath, trimmedPath); + searchDirectories.push(resolvedPath); + } + } else { + traceWarn('Warning: No workspace folders found for relative path:', trimmedPath); + } + } + } + + // Remove duplicates and return + const uniquePaths = Array.from(new Set(searchDirectories)); + traceLog( + 'getAllExtraSearchPaths completed. Total unique search directories:', + uniquePaths.length, + 'Paths:', + uniquePaths, + ); + return uniquePaths; +} + +/** + * Gets globalSearchPaths setting with proper validation. + * Only gets user-level (global) setting since this setting is application-scoped. + */ +function getGlobalSearchPaths(): string[] { + try { + const envConfig = getConfiguration('python-env'); + const inspection = envConfig.inspect('globalSearchPaths'); + + const globalPaths = inspection?.globalValue || []; + return untildifyArray(globalPaths); + } catch (error) { + traceError('Error getting globalSearchPaths:', error); + return []; + } +} + +/** + * Gets the most specific workspace-level setting available for workspaceSearchPaths. + */ +function getWorkspaceSearchPaths(): string[] { + try { + const envConfig = getConfiguration('python-env'); + const inspection = envConfig.inspect('workspaceSearchPaths'); + + if (inspection?.globalValue) { + traceError( + 'Error: python-env.workspaceSearchPaths is set at the user/global level, but this setting can only be set at the workspace or workspace folder level.', + ); + } + + // For workspace settings, prefer workspaceFolder > workspace + if (inspection?.workspaceFolderValue) { + return inspection.workspaceFolderValue; + } + + if (inspection?.workspaceValue) { + return inspection.workspaceValue; + } + + // Default empty array (don't use global value for workspace settings) + return []; + } catch (error) { + traceError('Error getting workspaceSearchPaths:', error); + return []; + } +} + export function getCacheDirectory(context: ExtensionContext): Uri { return Uri.joinPath(context.globalStorageUri, 'pythonLocator'); } @@ -374,10 +494,10 @@ export async function clearCacheDirectory(context: ExtensionContext): Promise { + return new NativePythonFinderImpl(outputChannel, await getNativePythonToolsPath(), api, getCacheDirectory(context)); } diff --git a/src/managers/common/pickers.ts b/src/managers/common/pickers.ts new file mode 100644 index 0000000..50a8269 --- /dev/null +++ b/src/managers/common/pickers.ts @@ -0,0 +1,275 @@ +import { QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, QuickPickItemKind, ThemeIcon, Uri } from 'vscode'; +import { launchBrowser } from '../../common/env.apis'; +import { Common, PackageManagement } from '../../common/localize'; +import { showInputBoxWithButtons, showQuickPickWithButtons, showTextDocument } from '../../common/window.apis'; +import { Installable } from './types'; + +const OPEN_BROWSER_BUTTON = { + iconPath: new ThemeIcon('globe'), + tooltip: Common.openInBrowser, +}; + +const OPEN_EDITOR_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: Common.openInEditor, +}; + +const EDIT_ARGUMENTS_BUTTON = { + iconPath: new ThemeIcon('add'), + tooltip: PackageManagement.editArguments, +}; + +function handleItemButton(uri?: Uri) { + if (uri) { + if (uri.scheme.toLowerCase().startsWith('http')) { + launchBrowser(uri); + } else { + showTextDocument(uri); + } + } +} + +interface PackageQuickPickItem extends QuickPickItem { + id: string; + uri?: Uri; + args?: string[]; +} + +function getDetail(i: Installable): string | undefined { + if (i.args && i.args.length > 0) { + if (i.args.length === 1 && i.args[0] === i.name) { + return undefined; + } + return i.args.join(' '); + } + return undefined; +} + +function installableToQuickPickItem(i: Installable): PackageQuickPickItem { + const detail = i.description ? getDetail(i) : undefined; + const description = i.description ? i.description : getDetail(i); + const buttons = i.uri + ? i.uri.scheme.startsWith('http') + ? [OPEN_BROWSER_BUTTON] + : [OPEN_EDITOR_BUTTON] + : undefined; + return { + label: i.displayName, + detail, + description, + buttons, + uri: i.uri, + args: i.args, + id: i.name, + }; +} + +async function enterPackageManually(filler?: string): Promise { + const input = await showInputBoxWithButtons({ + placeHolder: PackageManagement.enterPackagesPlaceHolder, + value: filler, + ignoreFocusOut: true, + showBackButton: true, + }); + return input?.split(' '); +} + +interface GroupingResult { + items: PackageQuickPickItem[]; + installedItems: PackageQuickPickItem[]; +} + +function groupByInstalled(items: PackageQuickPickItem[], installed?: string[]): GroupingResult { + const installedItems: PackageQuickPickItem[] = []; + const result: PackageQuickPickItem[] = []; + items.forEach((i) => { + if (installed?.find((p) => i.id === p)) { + installedItems.push(i); + } else { + result.push(i); + } + }); + const installedSeparator: PackageQuickPickItem = { + id: 'installed-sep', + label: PackageManagement.installed, + kind: QuickPickItemKind.Separator, + }; + const commonPackages: PackageQuickPickItem = { + id: 'common-packages-sep', + label: PackageManagement.commonPackages, + kind: QuickPickItemKind.Separator, + }; + return { + items: [installedSeparator, ...installedItems, commonPackages, ...result], + installedItems, + }; +} + +interface PackagesPickerResult { + install: string[]; + uninstall: string[]; +} + +function selectionsToResult(selections: string[], installed: string[]): PackagesPickerResult { + const install: string[] = selections; + const uninstall: string[] = []; + installed.forEach((i) => { + if (!selections.find((s) => i === s)) { + uninstall.push(i); + } + }); + return { + install, + uninstall, + }; +} + +export async function selectFromCommonPackagesToInstall( + common: Installable[], + installed: string[], + preSelected?: PackageQuickPickItem[] | undefined, + options?: { showBackButton?: boolean } | undefined, +): Promise { + const { installedItems, items } = groupByInstalled(common.map(installableToQuickPickItem), installed); + const preSelectedItems = items.filter((i) => (preSelected ?? installedItems).some((s) => s.id === i.id)); + let selected: PackageQuickPickItem | PackageQuickPickItem[] | undefined; + try { + selected = await showQuickPickWithButtons( + items as PackageQuickPickItem[], + { + placeHolder: PackageManagement.selectPackagesToInstall, + ignoreFocusOut: true, + canPickMany: true, + showBackButton: options?.showBackButton, + buttons: [EDIT_ARGUMENTS_BUTTON], + selected: preSelectedItems, + }, + undefined, + (e: QuickPickItemButtonEvent) => { + handleItemButton(e.item.uri); + }, + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (ex: any) { + if (ex === QuickInputButtons.Back) { + throw ex; + } else if (ex.button === EDIT_ARGUMENTS_BUTTON && ex.item) { + const parts: PackageQuickPickItem[] = Array.isArray(ex.item) ? ex.item : [ex.item]; + selected = [ + { + id: PackageManagement.enterPackageNames, + label: PackageManagement.enterPackageNames, + alwaysShow: true, + }, + ...parts, + ]; + } + } + + if (selected && Array.isArray(selected)) { + if (selected.find((s) => s.label === PackageManagement.enterPackageNames)) { + const filtered = selected.filter((s) => s.label !== PackageManagement.enterPackageNames); + try { + const selections = await enterPackageManually(); + if (selections) { + const selectedResult: PackagesPickerResult = selectionsToResult(selections, installed); + // only return the install part, since this button is only for adding to existing selection + return { install: selectedResult.install, uninstall: [] }; + } + return undefined; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + return selectFromCommonPackagesToInstall(common, installed, filtered); + } + return undefined; + } + } else { + return selectionsToResult( + selected.map((s) => s.id), + installed, + ); + } + } +} + +function getGroupedItems(items: Installable[]): PackageQuickPickItem[] { + const groups = new Map(); + const workspaceInstallable: Installable[] = []; + + items.forEach((i) => { + if (i.group) { + let group = groups.get(i.group); + if (!group) { + group = []; + groups.set(i.group, group); + } + group.push(i); + } else { + workspaceInstallable.push(i); + } + }); + + const result: PackageQuickPickItem[] = []; + groups.forEach((group, key) => { + result.push({ + id: key, + label: key, + kind: QuickPickItemKind.Separator, + }); + result.push(...group.map(installableToQuickPickItem)); + }); + + if (workspaceInstallable.length > 0) { + result.push({ + id: PackageManagement.workspaceDependencies, + label: PackageManagement.workspaceDependencies, + kind: QuickPickItemKind.Separator, + }); + result.push(...workspaceInstallable.map(installableToQuickPickItem)); + } + + return result; +} + +export async function selectFromInstallableToInstall( + installable: Installable[], + preSelected?: PackageQuickPickItem[], + options?: { showBackButton?: boolean } | undefined, +): Promise { + const items: PackageQuickPickItem[] = []; + + if (installable && installable.length > 0) { + items.push(...getGroupedItems(installable)); + } else { + return undefined; + } + + let preSelectedItems = items + .filter((i) => i.kind !== QuickPickItemKind.Separator) + .filter((i) => + preSelected?.find((s) => s.id === i.id && s.description === i.description && s.detail === i.detail), + ); + const selected = await showQuickPickWithButtons( + items, + { + placeHolder: PackageManagement.selectPackagesToInstall, + ignoreFocusOut: true, + canPickMany: true, + showBackButton: options?.showBackButton, + selected: preSelectedItems, + }, + undefined, + (e: QuickPickItemButtonEvent) => { + handleItemButton(e.item.uri); + }, + ); + + if (selected) { + if (Array.isArray(selected)) { + return { install: selected.flatMap((s) => s.args ?? []), uninstall: [] }; + } else { + return { install: selected.args ?? [], uninstall: [] }; + } + } + return undefined; +} diff --git a/src/managers/common/types.ts b/src/managers/common/types.ts new file mode 100644 index 0000000..ac1eac6 --- /dev/null +++ b/src/managers/common/types.ts @@ -0,0 +1,48 @@ +import { Uri } from 'vscode'; + +export interface Installable { + /** + * The name of the package, requirements, lock files, or step name. + */ + readonly name: string; + + /** + * The name of the package, requirements, pyproject.toml or any other project file, etc. + */ + readonly displayName: string; + + /** + * Arguments passed to the package manager to install the package. + * + * @example + * ['debugpy==1.8.7'] for `pip install debugpy==1.8.7`. + * ['--pre', 'debugpy'] for `pip install --pre debugpy`. + * ['-r', 'requirements.txt'] for `pip install -r requirements.txt`. + */ + readonly args?: string[]; + + /** + * Installable group name, this will be used to group installable items in the UI. + * + * @example + * `Requirements` for any requirements file. + * `Packages` for any package. + */ + readonly group?: string; + + /** + * Description about the installable item. This can also be path to the requirements, + * version of the package, or any other project file path. + */ + readonly description?: string; + + /** + * External Uri to the package on pypi or docs. + * @example + * https://pypi.org/project/debugpy/ for `debugpy`. + */ + readonly uri?: Uri; +} +export interface IDisposable { + dispose(): void | undefined | Promise; +} diff --git a/src/managers/common/utils.ts b/src/managers/common/utils.ts index 5aeb4ce..bffa42a 100644 --- a/src/managers/common/utils.ts +++ b/src/managers/common/utils.ts @@ -1,17 +1,13 @@ -import * as os from 'os'; -import { PythonEnvironment } from '../../api'; - -export function isWindows(): boolean { - return process.platform === 'win32'; -} - -export function untildify(path: string): string { - return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); -} - -export function getUserHomeDir(): string { - return os.homedir(); -} +import * as fs from 'fs-extra'; +import path from 'path'; +import { commands, ConfigurationTarget, l10n, window, workspace } from 'vscode'; +import { PythonCommandRunConfiguration, PythonEnvironment, PythonEnvironmentApi } from '../../api'; +import { traceLog, traceVerbose } from '../../common/logging'; +import { isWindows } from '../../common/utils/platformUtils'; +import { ShellConstants } from '../../features/common/shellConstants'; +import { getDefaultEnvManagerSetting, setDefaultEnvManagerBroken } from '../../features/settings/settingHelpers'; +import { PythonProjectManager } from '../../internal.api'; +import { Installable } from './types'; export function noop() { // do nothing @@ -28,6 +24,7 @@ export function shortVersion(version: string): string { } return version; } + export function isGreater(a: string | undefined, b: string | undefined): boolean { if (!a && !b) { return false; @@ -55,7 +52,7 @@ export function isGreater(a: string | undefined, b: string | undefined): boolean return false; } } - } catch (ex) { + } catch { return false; } return false; @@ -86,3 +83,219 @@ export function getLatest(collection: PythonEnvironment[]): PythonEnvironment | } return latest; } + +export function mergePackages(common: Installable[], installed: string[]): Installable[] { + const notInCommon = installed.filter((pkg) => !common.some((c) => c.name === pkg)); + return common + .concat(notInCommon.map((pkg) => ({ name: pkg, displayName: pkg }))) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +export function pathForGitBash(binPath: string): string { + return isWindows() ? binPath.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1') : binPath; +} + +/** + * Compares two semantic version strings. Support sonly simple 1.1.1 style versions. + * @param version1 First version + * @param version2 Second version + * @returns -1 if version1 < version2, 0 if equal, 1 if version1 > version2 + */ +export function compareVersions(version1: string, version2: string): number { + const v1Parts = version1.split('.').map(Number); + const v2Parts = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { + const v1Part = v1Parts[i] || 0; + const v2Part = v2Parts[i] || 0; + + if (v1Part > v2Part) { + return 1; + } + if (v1Part < v2Part) { + return -1; + } + } + + return 0; +} + +export async function getShellActivationCommands(binDir: string): Promise<{ + shellActivation: Map; + shellDeactivation: Map; +}> { + const shellActivation: Map = new Map(); + const shellDeactivation: Map = new Map(); + + if (isWindows()) { + shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]); + shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]); + } else { + shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set('unknown', [{ executable: 'deactivate' }]); + } + + shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.GITBASH, [ + { executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] }, + ]); + shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]); + shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]); + + if (await fs.pathExists(path.join(binDir, 'Activate.ps1'))) { + shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]); + shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); + } else if (await fs.pathExists(path.join(binDir, 'activate.ps1'))) { + shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]); + shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.bat'))) { + shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]); + shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.csh'))) { + shellActivation.set(ShellConstants.CSH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]); + shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]); + + shellActivation.set(ShellConstants.FISH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]); + shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.fish'))) { + shellActivation.set(ShellConstants.FISH, [ + { executable: 'source', args: [path.join(binDir, `activate.fish`)] }, + ]); + shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.xsh'))) { + shellActivation.set(ShellConstants.XONSH, [ + { executable: 'source', args: [path.join(binDir, `activate.xsh`)] }, + ]); + shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]); + } + + if (await fs.pathExists(path.join(binDir, 'activate.nu'))) { + shellActivation.set(ShellConstants.NU, [ + { executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] }, + ]); + shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]); + } + return { + shellActivation, + shellDeactivation, + }; +} + +// Tracks if the broken defaultEnvManager error message has been shown this session +let hasShownBrokenDefaultEnvManagerError = false; + +/** + * Checks if the given managerId is set as the default environment manager for the project. + * If so, marks the default manager as broken, refreshes environments, and shows an error message to the user. + * The error message offers to reset the setting, view the setting, or close. + * The error message is only shown once per session. + * + * @param managerId The environment manager id to check. + * @param projectManager The Python project manager instance. + * @param api The Python environment API instance. + */ +export async function notifyMissingManagerIfDefault( + managerId: string, + projectManager: PythonProjectManager, + api: PythonEnvironmentApi, +) { + const defaultEnvManager = getDefaultEnvManagerSetting(projectManager); + if (defaultEnvManager === managerId) { + if (hasShownBrokenDefaultEnvManagerError) { + return; + } + hasShownBrokenDefaultEnvManagerError = true; + setDefaultEnvManagerBroken(true); + await api.refreshEnvironments(undefined); + window + .showErrorMessage( + l10n.t( + "The default environment manager is set to '{0}', but the {1} executable could not be found.", + defaultEnvManager, + managerId.split(':')[1], + ), + l10n.t('Reset setting'), + l10n.t('View setting'), + l10n.t('Close'), + ) + .then(async (selection) => { + if (selection === 'Reset setting') { + const result = await removeFirstDefaultEnvManagerSettingDetailed(managerId); + if (!result.found) { + window + .showErrorMessage( + l10n.t( + "Could not find a setting for 'defaultEnvManager' set to '{0}' to reset.", + managerId, + ), + l10n.t('Open settings'), + l10n.t('Close'), + ) + .then((sel) => { + if (sel === 'Open settings') { + commands.executeCommand( + 'workbench.action.openSettings', + 'python-envs.defaultEnvManager', + ); + } + }); + } + } + if (selection === 'View setting') { + commands.executeCommand('workbench.action.openSettings', 'python-envs.defaultEnvManager'); + } + }); + } +} + +/** + * Removes the first occurrence of 'defaultEnvManager' set to managerId, returns where it was removed, and logs the action. + * @param managerId The manager id to match and remove. + * @returns { found: boolean, scope?: string } + */ +export async function removeFirstDefaultEnvManagerSettingDetailed( + managerId: string, +): Promise<{ found: boolean; scope?: string }> { + const config = workspace.getConfiguration('python-envs'); + const inspect = config.inspect('defaultEnvManager'); + + // Workspace folder settings (multi-root) + if (inspect?.workspaceFolderValue !== undefined && inspect.workspaceFolderValue === managerId) { + await config.update('defaultEnvManager', undefined, ConfigurationTarget.WorkspaceFolder); + traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace Folder settings."); + return { found: true, scope: 'Workspace Folder' }; + } + // Workspace settings + if (inspect?.workspaceValue !== undefined && inspect.workspaceValue === managerId) { + await config.update('defaultEnvManager', undefined, ConfigurationTarget.Workspace); + traceLog("[python-envs] Removed 'defaultEnvManager' from Workspace settings."); + return { found: true, scope: 'Workspace' }; + } + // User/global settings + if (inspect?.globalValue !== undefined && inspect.globalValue === managerId) { + await config.update('defaultEnvManager', undefined, ConfigurationTarget.Global); + traceLog("[python-envs] Removed 'defaultEnvManager' from User/Global settings."); + return { found: true, scope: 'User/Global' }; + } + // No matching setting found + traceVerbose(`[python-envs] Could not find 'defaultEnvManager' set to '${managerId}' in any scope.`); + return { found: false }; +} diff --git a/src/managers/conda/condaEnvManager.ts b/src/managers/conda/condaEnvManager.ts index d00ef02..d43743d 100644 --- a/src/managers/conda/condaEnvManager.ts +++ b/src/managers/conda/condaEnvManager.ts @@ -1,6 +1,8 @@ +import * as fs from 'fs-extra'; import * as path from 'path'; -import { Disposable, EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, Uri, window } from 'vscode'; +import { Disposable, EventEmitter, l10n, LogOutputChannel, MarkdownString, ProgressLocation, Uri } from 'vscode'; import { + CreateEnvironmentOptions, CreateEnvironmentScope, DidChangeEnvironmentEventArgs, DidChangeEnvironmentsEventArgs, @@ -12,29 +14,37 @@ import { PythonEnvironment, PythonEnvironmentApi, PythonProject, + QuickCreateConfig, RefreshEnvironmentsScope, ResolveEnvironmentContext, SetEnvironmentScope, } from '../../api'; +import { CondaStrings } from '../../common/localize'; +import { traceError } from '../../common/logging'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { showErrorMessage, withProgress } from '../../common/window.apis'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { CondaSourcingStatus } from './condaSourcingUtils'; import { + checkForNoPythonCondaEnvironment, clearCondaCache, createCondaEnvironment, deleteCondaEnvironment, + generateName, getCondaForGlobal, getCondaForWorkspace, + getDefaultCondaPrefix, + quickCreateConda, refreshCondaEnvs, resolveCondaPath, setCondaForGlobal, setCondaForWorkspace, + setCondaForWorkspaces, } from './condaUtils'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { NativePythonFinder } from '../common/nativePythonFinder'; -import { createDeferred, Deferred } from '../../common/utils/deferred'; export class CondaEnvManager implements EnvironmentManager, Disposable { private collection: PythonEnvironment[] = []; private fsPathToEnv: Map = new Map(); - private disposablesMap: Map = new Map(); private globalEnv: PythonEnvironment | undefined; private readonly _onDidChangeEnvironment = new EventEmitter(); @@ -43,6 +53,8 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { private readonly _onDidChangeEnvironments = new EventEmitter(); public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + public sourcingInformation: CondaSourcingStatus | undefined; + constructor( private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi, @@ -51,19 +63,19 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { this.name = 'conda'; this.displayName = 'Conda'; this.preferredPackageManagerId = 'ms-python.python:conda'; - this.description = 'Conda environment manager'; - this.tooltip = 'Conda environment manager'; + this.tooltip = new MarkdownString(CondaStrings.condaManager, true); } name: string; displayName: string; - preferredPackageManagerId: string = 'ms-python.python:conda'; - description: string; + preferredPackageManagerId: string; + description?: string; tooltip: string | MarkdownString; - iconPath: IconPath; + iconPath?: IconPath; public dispose() { - this.disposablesMap.forEach((d) => d.dispose()); + this.collection = []; + this.fsPathToEnv.clear(); } private _initialized: Deferred | undefined; @@ -74,10 +86,10 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { this._initialized = createDeferred(); - await window.withProgress( + await withProgress( { location: ProgressLocation.Window, - title: 'Discovering Conda environments', + title: CondaStrings.condaDiscovering, }, async () => { this.collection = await refreshCondaEnvs(false, this.nativeFinder, this.api, this.log, this); @@ -114,29 +126,68 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { return []; } - async create(context: CreateEnvironmentScope): Promise { + quickCreateConfig(): QuickCreateConfig | undefined { + if (!this.globalEnv) { + return undefined; + } + + return { + description: l10n.t('Create a conda environment'), + detail: l10n.t('Uses Python version {0} and installs workspace dependencies.', this.globalEnv.version), + }; + } + + async create( + context: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise { try { - const result = await createCondaEnvironment( - this.api, - this.log, - this, - context === 'global' ? undefined : context, - ); - if (!result) { - return undefined; + let result: PythonEnvironment | undefined; + if (options?.quickCreate) { + let envRoot: string | undefined = undefined; + let name: string | undefined = './.conda'; + if (context === 'global' || (Array.isArray(context) && context.length > 1)) { + envRoot = await getDefaultCondaPrefix(); + name = await generateName(envRoot); + } else { + const folder = this.api.getPythonProject(context instanceof Uri ? context : context[0]); + envRoot = folder?.uri.fsPath; + } + if (!envRoot) { + showErrorMessage(CondaStrings.quickCreateCondaNoEnvRoot); + return undefined; + } + if (!name) { + showErrorMessage(CondaStrings.quickCreateCondaNoName); + return undefined; + } + result = await quickCreateConda(this.api, this.log, this, envRoot, name, options?.additionalPackages); + } else { + result = await createCondaEnvironment( + this.api, + this.log, + this, + context === 'global' ? undefined : context, + ); } - this.disposablesMap.set( - result.envId.id, - new Disposable(() => { - this.collection = this.collection.filter((env) => env.envId.id !== result.envId.id); - Array.from(this.fsPathToEnv.entries()) - .filter(([, env]) => env.envId.id === result.envId.id) - .forEach(([uri]) => this.fsPathToEnv.delete(uri)); - this.disposablesMap.delete(result.envId.id); - }), - ); - this.collection.push(result); - this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: result }]); + if (result) { + this.addEnvironment(result); + + // If the environment is inside the workspace, add a .gitignore file + try { + const projectUris = this.api.getPythonProjects().map((p) => p.uri.fsPath); + const envPath = result.environmentPath?.fsPath; + if (envPath && projectUris.some((root) => envPath.startsWith(root))) { + const gitignorePath = path.join(envPath, '.gitignore'); + await fs.writeFile(gitignorePath, '*\n', { flag: 'w' }); + } + } catch (err) { + traceError( + `Failed to create .gitignore in conda env: ${err instanceof Error ? err.message : String(err)}`, + ); + } + } + return result; } catch (error) { this.log.error('Failed to create conda environment:', error); @@ -144,12 +195,28 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { } } + private addEnvironment(environment: PythonEnvironment, raiseEvent: boolean = true): void { + this.collection.push(environment); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: environment }]); + } + } + + private removeEnvironment(environment: PythonEnvironment, raiseEvent: boolean = true): void { + this.collection = this.collection.filter((env) => env.envId.id !== environment.envId.id); + Array.from(this.fsPathToEnv.entries()) + .filter(([, env]) => env.envId.id === environment.envId.id) + .forEach(([uri]) => this.fsPathToEnv.delete(uri)); + if (raiseEvent) { + this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.remove, environment }]); + } + } + async remove(context: PythonEnvironment): Promise { try { const projects = this.getProjectsForEnvironment(context); + this.removeEnvironment(context, false); await deleteCondaEnvironment(context, this.log); - this.disposablesMap.get(context.envId.id)?.dispose(); - setImmediate(() => { this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.remove, environment: context }]); projects.forEach((project) => @@ -162,13 +229,10 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { } async refresh(context: RefreshEnvironmentsScope): Promise { if (context === undefined) { - this.disposablesMap.forEach((d) => d.dispose()); - this.disposablesMap.clear(); - - await window.withProgress( + await withProgress( { location: ProgressLocation.Window, - title: 'Refreshing Conda Environments', + title: CondaStrings.condaRefreshingEnvs, }, async () => { this.log.info('Refreshing Conda Environments'); @@ -205,23 +269,70 @@ export class CondaEnvManager implements EnvironmentManager, Disposable { return this.globalEnv; } + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment | undefined): Promise { + const checkedEnv = environment + ? await checkForNoPythonCondaEnvironment(this.nativeFinder, this, environment, this.api, this.log) + : undefined; + if (scope === undefined) { - await setCondaForGlobal(environment?.environmentPath?.fsPath); + await setCondaForGlobal(checkedEnv?.environmentPath?.fsPath); } else if (scope instanceof Uri) { const folder = this.api.getPythonProject(scope); const fsPath = folder?.uri?.fsPath ?? scope.fsPath; if (fsPath) { - if (environment) { - this.fsPathToEnv.set(fsPath, environment); - await setCondaForWorkspace(fsPath, environment.environmentPath.fsPath); + if (checkedEnv) { + this.fsPathToEnv.set(fsPath, checkedEnv); } else { this.fsPathToEnv.delete(fsPath); - await setCondaForWorkspace(fsPath, undefined); } + await setCondaForWorkspace(fsPath, checkedEnv?.environmentPath.fsPath); } + } else if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (checkedEnv) { + this.fsPathToEnv.set(p.uri.fsPath, checkedEnv); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setCondaForWorkspaces( + projects.map((p) => p.uri.fsPath), + checkedEnv?.environmentPath.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== checkedEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: checkedEnv }); + } + }); + } + + if (environment && checkedEnv && checkedEnv.envId.id !== environment.envId.id) { + this.removeEnvironment(environment, false); + this.addEnvironment(checkedEnv, false); + setImmediate(() => { + this._onDidChangeEnvironments.fire([ + { kind: EnvironmentChangeKind.remove, environment }, + { kind: EnvironmentChangeKind.add, environment: checkedEnv }, + ]); + const uri = scope ? (scope instanceof Uri ? scope : scope[0]) : undefined; + this._onDidChangeEnvironment.fire({ uri, old: environment, new: checkedEnv }); + }); } - return; } async resolve(context: ResolveEnvironmentContext): Promise { diff --git a/src/managers/conda/condaPackageManager.ts b/src/managers/conda/condaPackageManager.ts index 7894e5a..c012ea9 100644 --- a/src/managers/conda/condaPackageManager.ts +++ b/src/managers/conda/condaPackageManager.ts @@ -1,25 +1,26 @@ -import * as path from 'path'; import { + CancellationError, Disposable, Event, EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, - Uri, - window, } from 'vscode'; import { DidChangePackagesEventArgs, IconPath, Package, PackageChangeKind, - PackageInstallOptions, + PackageManagementOptions, PackageManager, PythonEnvironment, PythonEnvironmentApi, } from '../../api'; -import { installPackages, refreshPackages, uninstallPackages } from './condaUtils'; +import { showErrorMessageWithLogs } from '../../common/errors/utils'; +import { CondaStrings } from '../../common/localize'; +import { withProgress } from '../../common/window.apis'; +import { getCommonCondaPackagesToInstall, managePackages, refreshPackages } from './condaUtils'; function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { const changes: { kind: PackageChangeKind; pkg: Package }[] = []; @@ -38,77 +39,78 @@ export class CondaPackageManager implements PackageManager, Disposable { private packages: Map = new Map(); - constructor(public readonly api: PythonEnvironmentApi, public readonly logOutput: LogOutputChannel) { + constructor(public readonly api: PythonEnvironmentApi, public readonly log: LogOutputChannel) { this.name = 'conda'; this.displayName = 'Conda'; - this.description = 'Conda package manager'; - this.tooltip = 'Conda package manager'; + this.description = CondaStrings.condaPackageMgr; + this.tooltip = CondaStrings.condaPackageMgr; } name: string; - displayName?: string | undefined; - description?: string | undefined; - tooltip?: string | MarkdownString | undefined; - iconPath?: IconPath | undefined; + displayName?: string; + description?: string; + tooltip?: string | MarkdownString; + iconPath?: IconPath; - async install(environment: PythonEnvironment, packages: string[], options: PackageInstallOptions): Promise { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Installing packages', - }, - async () => { - try { - const before = this.packages.get(environment.envId.id) ?? []; - const after = await installPackages(environment, packages, options, this.api, this); - const changes = getChanges(before, after); - this.packages.set(environment.envId.id, after); - this._onDidChangePackages.fire({ environment: environment, manager: this, changes }); - } catch (e) { - this.logOutput.error('Error installing packages', e); - setImmediate(async () => { - const result = await window.showErrorMessage('Error installing packages', 'View Output'); - if (result === 'View Output') { - this.logOutput.show(); - } - }); - } - }, - ); - } + async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { + let toInstall: string[] = [...(options.install ?? [])]; + let toUninstall: string[] = [...(options.uninstall ?? [])]; - async uninstall(environment: PythonEnvironment, packages: Package[] | string[]): Promise { - await window.withProgress( + if (toInstall.length === 0 && toUninstall.length === 0) { + const result = await getCommonCondaPackagesToInstall(environment, options, this.api); + if (result) { + toInstall = result.install; + toUninstall = result.uninstall; + } else { + return; + } + } + + const manageOptions = { + ...options, + install: toInstall, + uninstall: toUninstall, + }; + await withProgress( { location: ProgressLocation.Notification, - title: 'Uninstalling packages', + title: CondaStrings.condaInstallingPackages, + cancellable: true, }, - async () => { + async (_progress, token) => { try { const before = this.packages.get(environment.envId.id) ?? []; - const after = await uninstallPackages(environment, packages, this.api, this); + const after = await managePackages(environment, manageOptions, this.api, this, token, this.log); const changes = getChanges(before, after); this.packages.set(environment.envId.id, after); this._onDidChangePackages.fire({ environment: environment, manager: this, changes }); } catch (e) { - this.logOutput.error('Error uninstalling packages', e); + if (e instanceof CancellationError) { + throw e; + } + + this.log.error('Error installing packages', e); setImmediate(async () => { - const result = await window.showErrorMessage('Error installing packages', 'View Output'); - if (result === 'View Output') { - this.logOutput.show(); - } + await showErrorMessageWithLogs(CondaStrings.condaInstallError, this.log); }); } }, ); } - async refresh(context: PythonEnvironment): Promise { - await window.withProgress( + + async refresh(environment: PythonEnvironment): Promise { + await withProgress( { location: ProgressLocation.Window, - title: 'Refreshing packages', + title: CondaStrings.condaRefreshingPackages, }, async () => { - this.packages.set(context.envId.id, await refreshPackages(context, this.api, this)); + const before = this.packages.get(environment.envId.id) ?? []; + const after = await refreshPackages(environment, this.api, this); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + if (changes.length > 0) { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } }, ); } diff --git a/src/managers/conda/condaSourcingUtils.ts b/src/managers/conda/condaSourcingUtils.ts new file mode 100644 index 0000000..a8f888b --- /dev/null +++ b/src/managers/conda/condaSourcingUtils.ts @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { traceError, traceInfo, traceVerbose } from '../../common/logging'; +import { isWindows } from '../../common/utils/platformUtils'; + +/** + * Represents the status of conda sourcing in the current environment + */ +export class CondaSourcingStatus { + /** + * Creates a new CondaSourcingStatus instance + * @param condaPath Path to the conda installation + * @param condaFolder Path to the conda installation folder (derived from condaPath) + * @param isActiveOnLaunch Whether conda was activated before VS Code launch + * @param globalSourcingScript Path to the global sourcing script (if exists) + * @param shellSourcingScripts List of paths to shell-specific sourcing scripts + */ + constructor( + public readonly condaPath: string, + public readonly condaFolder: string, + public isActiveOnLaunch?: boolean, + public globalSourcingScript?: string, + public shellSourcingScripts?: string[], + ) {} + + /** + * Returns a formatted string representation of the conda sourcing status + */ + toString(): string { + const lines: string[] = []; + lines.push('Conda Sourcing Status:'); + lines.push(`├─ Conda Path: ${this.condaPath}`); + lines.push(`├─ Conda Folder: ${this.condaFolder}`); + lines.push(`├─ Active on Launch: ${this.isActiveOnLaunch ?? 'false'}`); + + if (this.globalSourcingScript) { + lines.push(`├─ Global Sourcing Script: ${this.globalSourcingScript}`); + } + + if (this.shellSourcingScripts?.length) { + lines.push('└─ Shell-specific Sourcing Scripts:'); + this.shellSourcingScripts.forEach((script, index, array) => { + const isLast = index === array.length - 1; + if (script) { + // Only include scripts that exist + lines.push(` ${isLast ? '└─' : '├─'} ${script}`); + } + }); + } else { + lines.push('└─ No Shell-specific Sourcing Scripts Found'); + } + + return lines.join('\n'); + } +} + +/** + * Constructs the conda sourcing status for a given conda installation + * @param condaPath The path to the conda executable + * @returns A CondaSourcingStatus object containing: + * - Whether conda was active when VS Code launched + * - Path to global sourcing script (if found) + * - Paths to shell-specific sourcing scripts (if found) + * + * This function checks: + * 1. If conda is already active in the current shell (CONDA_SHLVL) + * 2. Location of the global activation script + * 3. Location of shell-specific activation scripts + */ +export async function constructCondaSourcingStatus(condaPath: string): Promise { + const condaFolder = path.dirname(path.dirname(condaPath)); + let sourcingStatus = new CondaSourcingStatus(condaPath, condaFolder); + + // The `conda_shlvl` value indicates whether conda is properly initialized in the current shell: + // - `-1`: Conda has never been sourced + // - `undefined`: No shell level information available + // - `0 or higher`: Conda is properly sourced in the shell + const condaShlvl = process.env.CONDA_SHLVL; + if (condaShlvl && parseInt(condaShlvl) >= 0) { + sourcingStatus.isActiveOnLaunch = true; + // if activation already occurred, no need to find further scripts + return sourcingStatus; + } + + // Attempt to find the GLOBAL conda sourcing script + const globalSourcingScript: string | undefined = await findGlobalSourcingScript(sourcingStatus.condaFolder); + if (globalSourcingScript) { + sourcingStatus.globalSourcingScript = globalSourcingScript; + // note: future iterations could decide to exit here instead of continuing to generate all the other activation scripts + } + + // find and save all of the shell specific sourcing scripts + sourcingStatus.shellSourcingScripts = await findShellSourcingScripts(sourcingStatus); + + return sourcingStatus; +} + +/** + * Finds the global conda activation script for the given conda installation + * @param condaPath The path to the conda executable + * @returns The path to the global activation script if it exists, undefined otherwise + * + * On Windows, this will look for 'Scripts/activate.bat' + * On Unix systems, this will look for 'bin/activate' + */ +export async function findGlobalSourcingScript(condaFolder: string): Promise { + const sourcingScript = isWindows() + ? path.join(condaFolder, 'Scripts', 'activate.bat') + : path.join(condaFolder, 'bin', 'activate'); + + if (await fse.pathExists(sourcingScript)) { + traceInfo(`Found global conda sourcing script at: ${sourcingScript}`); + return sourcingScript; + } else { + traceInfo(`No global conda sourcing script found. at: ${sourcingScript}`); + return undefined; + } +} + +export async function findShellSourcingScripts(sourcingStatus: CondaSourcingStatus): Promise { + const logs: string[] = []; + logs.push('=== Conda Sourcing Shell Script Search ==='); + + let ps1Script: string | undefined; + let shScript: string | undefined; + let cmdActivate: string | undefined; + + try { + // Search for PowerShell hook script (conda-hook.ps1) + logs.push('Searching for PowerShell hook script...'); + try { + ps1Script = await getCondaHookPs1Path(sourcingStatus.condaFolder); + logs.push(` Path: ${ps1Script ?? '✗ Not found'}`); + } catch (err) { + logs.push( + ` Error during PowerShell script search: ${err instanceof Error ? err.message : 'Unknown error'}`, + ); + } + + // Search for Shell script (conda.sh) + logs.push('\nSearching for Shell script...'); + try { + shScript = await getCondaShPath(sourcingStatus.condaFolder); + logs.push(` Path: ${shScript ?? '✗ Not found'}`); + } catch (err) { + logs.push(` Error during Shell script search: ${err instanceof Error ? err.message : 'Unknown error'}`); + } + + // Search for Windows CMD script (activate.bat) + logs.push('\nSearching for Windows CMD script...'); + try { + cmdActivate = await getCondaBatActivationFile(sourcingStatus.condaPath); + logs.push(` Path: ${cmdActivate ?? '✗ Not found'}`); + } catch (err) { + logs.push(` Error during CMD script search: ${err instanceof Error ? err.message : 'Unknown error'}`); + } + } catch (error) { + logs.push(`\nCritical error during script search: ${error instanceof Error ? error.message : 'Unknown error'}`); + } finally { + logs.push('\nSearch Summary:'); + logs.push(` PowerShell: ${ps1Script ? '✓' : '✗'}`); + logs.push(` Shell: ${shScript ? '✓' : '✗'}`); + logs.push(` CMD: ${cmdActivate ? '✓' : '✗'}`); + logs.push('============================'); + + // Log everything at once + traceVerbose(logs.join('\n')); + } + + return [ps1Script, shScript, cmdActivate] as string[]; +} + +/** + * Returns the best guess path to conda-hook.ps1 given a conda executable path. + * + * Searches for conda-hook.ps1 in these locations (relative to the conda root): + * - shell/condabin/ + * - Library/shell/condabin/ + * - condabin/ + * - etc/profile.d/ + */ +export async function getCondaHookPs1Path(condaFolder: string): Promise { + // Create the promise for finding the hook path + const hookPathPromise = (async () => { + const condaRootCandidates: string[] = [ + path.join(condaFolder, 'shell', 'condabin'), + path.join(condaFolder, 'Library', 'shell', 'condabin'), + path.join(condaFolder, 'condabin'), + path.join(condaFolder, 'etc', 'profile.d'), + ]; + + const checks = condaRootCandidates.map(async (hookSearchDir) => { + const candidate = path.join(hookSearchDir, 'conda-hook.ps1'); + if (await fse.pathExists(candidate)) { + traceInfo(`Conda hook found at: ${candidate}`); + return candidate; + } + return undefined; + }); + const results = await Promise.all(checks); + const found = results.find(Boolean); + if (found) { + return found as string; + } + return undefined; + })(); + + return hookPathPromise; +} + +/** + * Helper function that checks for a file in a list of locations. + * Returns the first location where the file exists, or undefined if not found. + */ +async function findFileInLocations(locations: string[], description: string): Promise { + for (const location of locations) { + if (await fse.pathExists(location)) { + traceInfo(`${description} found in ${location}`); + return location; + } + } + return undefined; +} + +/** + * Returns the path to conda.sh given a conda executable path. + * + * Searches for conda.sh in these locations (relative to the conda root): + * - etc/profile.d/conda.sh + * - shell/etc/profile.d/conda.sh + * - Library/etc/profile.d/conda.sh + * - lib/pythonX.Y/site-packages/conda/shell/etc/profile.d/conda.sh + * - site-packages/conda/shell/etc/profile.d/conda.sh + * Also checks some system-level locations + */ +async function getCondaShPath(condaFolder: string): Promise { + // Create the promise for finding the conda.sh path + const shPathPromise = (async () => { + // First try standard conda installation locations + const standardLocations = [ + path.join(condaFolder, 'etc', 'profile.d', 'conda.sh'), + path.join(condaFolder, 'shell', 'etc', 'profile.d', 'conda.sh'), + path.join(condaFolder, 'Library', 'etc', 'profile.d', 'conda.sh'), + ]; + + // Check standard locations first + const standardLocation = await findFileInLocations(standardLocations, 'conda.sh'); + if (standardLocation) { + return standardLocation; + } + + // If not found in standard locations, try pip install locations + // First, find all python* directories in lib + let pythonDirs: string[] = []; + const libPath = path.join(condaFolder, 'lib'); + try { + const dirs = await fse.readdir(libPath); + pythonDirs = dirs.filter((dir) => dir.startsWith('python')); + } catch (err) { + traceVerbose(`No lib directory found at ${libPath}, ${err}`); + } + + const pipInstallLocations = [ + ...pythonDirs.map((ver) => + path.join(condaFolder, 'lib', ver, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), + ), + path.join(condaFolder, 'site-packages', 'conda', 'shell', 'etc', 'profile.d', 'conda.sh'), + ]; + + // Check pip install locations + const pipLocation = await findFileInLocations(pipInstallLocations, 'conda.sh'); + if (pipLocation) { + traceError( + 'WARNING: conda.sh was found in a pip install location. ' + + 'This is not a supported configuration and may be deprecated in the future. ' + + 'Please install conda in a standard location. ' + + 'See https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html for proper installation instructions.', + ); + return pipLocation; + } + return undefined; + })(); + + return shPathPromise; +} + +/** + * Returns the path to the Windows batch activation file (activate.bat) for conda + * @param condaPath The path to the conda executable + * @returns The path to activate.bat if it exists in the same directory as conda.exe, undefined otherwise + * + * This file is used specifically for CMD.exe activation on Windows systems. + * It should be located in the same directory as the conda executable. + */ +async function getCondaBatActivationFile(condaPath: string): Promise { + const cmdActivate = path.join(path.dirname(condaPath), 'activate.bat'); + if (await fse.pathExists(cmdActivate)) { + return cmdActivate; + } + return undefined; +} + +/** + * Returns the path to the local conda activation script + * @param condaPath The path to the conda executable + * @returns Promise that resolves to: + * - The path to the local 'activate' script if it exists in the same directory as conda + * - undefined if the script is not found + * + * This function checks for a local 'activate' script in the same directory as the conda executable. + * This script is used for direct conda activation without shell-specific configuration. + */ + +const knownSourcingScriptCache: string[] = []; +export async function getLocalActivationScript(condaPath: string): Promise { + // Define all possible paths to check + const paths = [ + // Direct path + isWindows() ? path.join(condaPath, 'Scripts', 'activate') : path.join(condaPath, 'bin', 'activate'), + // One level up + isWindows() + ? path.join(path.dirname(condaPath), 'Scripts', 'activate') + : path.join(path.dirname(condaPath), 'bin', 'activate'), + // Two levels up + isWindows() + ? path.join(path.dirname(path.dirname(condaPath)), 'Scripts', 'activate') + : path.join(path.dirname(path.dirname(condaPath)), 'bin', 'activate'), + ]; + + // Check each path in sequence + for (const sourcingScript of paths) { + // Check if any of the paths are in the cache + if (knownSourcingScriptCache.includes(sourcingScript)) { + traceVerbose(`Found local activation script in cache at: ${sourcingScript}`); + return sourcingScript; + } + try { + const exists = await fse.pathExists(sourcingScript); + if (exists) { + traceInfo(`Found local activation script at: ${sourcingScript}, adding to cache.`); + knownSourcingScriptCache.push(sourcingScript); + return sourcingScript; + } + } catch (err) { + traceError(`Error checking for local activation script at ${sourcingScript}: ${err}`); + continue; + } + } + + traceVerbose('No local activation script found in any of the expected locations'); + return undefined; +} diff --git a/src/managers/conda/condaStepBasedFlow.ts b/src/managers/conda/condaStepBasedFlow.ts new file mode 100644 index 0000000..6cf6ce8 --- /dev/null +++ b/src/managers/conda/condaStepBasedFlow.ts @@ -0,0 +1,334 @@ +import * as fse from 'fs-extra'; +import * as path from 'path'; +import { l10n, LogOutputChannel, QuickInputButtons, QuickPickItem, Uri } from 'vscode'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi } from '../../api'; +import { CondaStrings } from '../../common/localize'; +import { showInputBoxWithButtons, showQuickPickWithButtons } from '../../common/window.apis'; +import { + createNamedCondaEnvironment, + createPrefixCondaEnvironment, + getLocation, + getName, + trimVersionToMajorMinor, +} from './condaUtils'; + +// Recommended Python version for Conda environments +const RECOMMENDED_CONDA_PYTHON = '3.11.11'; + +/** + * State interface for the Conda environment creation flow. + * + * This keeps track of all user selections throughout the flow, + * allowing the wizard to maintain context when navigating backwards. + */ +interface CondaCreationState { + // Type of Conda environment to create (named or prefix) + envType?: string; + + // Python version to install in the environment + pythonVersion?: string; + + // For named environments + envName?: string; + + // For prefix environments + prefix?: string; + fsPath?: string; + + // Additional context + uris?: Uri[]; + + // API reference for Python environment operations + api: PythonEnvironmentApi; +} + +/** + * Type definition for step functions in the wizard-like flow. + * + * Each step function: + * 1. Takes the current state as input + * 2. Interacts with the user through UI + * 3. Updates the state with new data + * 4. Returns the next step function to execute or null if flow is complete + */ +type StepFunction = (state: CondaCreationState) => Promise; + +/** + * Step 1: Select environment type (named or prefix) + */ +async function selectEnvironmentType(state: CondaCreationState): Promise { + try { + // Skip this step if we have multiple URIs (force named environment) + if (state.uris && state.uris.length > 1) { + state.envType = 'Named'; + return selectPythonVersion; + } + + const selection = (await showQuickPickWithButtons( + [ + { label: CondaStrings.condaNamed, description: CondaStrings.condaNamedDescription }, + { label: CondaStrings.condaPrefix, description: CondaStrings.condaPrefixDescription }, + ], + { + placeHolder: CondaStrings.condaSelectEnvType, + ignoreFocusOut: true, + showBackButton: true, + }, + )) as QuickPickItem | undefined; + + if (!selection) { + return null; + } + + state.envType = selection.label; + + // Next step: select Python version + return selectPythonVersion; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // This is the first step, so return null to exit the flow + return null; + } + throw ex; + } +} + +/** + * Step 2: Select Python version + */ +async function selectPythonVersion(state: CondaCreationState): Promise { + try { + const api = state.api; + if (!api) { + return null; + } + + const envs = await api.getEnvironments('global'); + let versions = Array.from( + new Set( + envs + .map((env: PythonEnvironment) => env.version) + .filter(Boolean) + .map((v: string) => trimVersionToMajorMinor(v)), // cut to 3 digits + ), + ); + + // Sort versions by major version (descending), ignoring minor/patch for simplicity + const parseMajorMinor = (v: string) => { + const m = v.match(/^(\\d+)(?:\\.(\\d+))?/); + return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + }; + + versions = versions.sort((a, b) => { + const pa = parseMajorMinor(a as string); + const pb = parseMajorMinor(b as string); + if (pa.major !== pb.major) { + return pb.major - pa.major; + } // desc by major + return pb.minor - pa.minor; // desc by minor + }); + + if (!versions || versions.length === 0) { + versions = ['3.13', '3.12', '3.11', '3.10', '3.9']; + } + + const items: QuickPickItem[] = versions.map((v: unknown) => ({ + label: v === RECOMMENDED_CONDA_PYTHON ? `$(star-full) Python` : 'Python', + description: String(v), + })); + + const selection = await showQuickPickWithButtons(items, { + placeHolder: l10n.t('Select the version of Python to install in the environment'), + matchOnDescription: true, + ignoreFocusOut: true, + showBackButton: true, + }); + + if (!selection) { + return null; + } + + state.pythonVersion = (selection as QuickPickItem).description; + + // Next step depends on environment type + return state.envType === 'Named' ? enterEnvironmentName : selectLocation; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to environment type selection + return selectEnvironmentType; + } + throw ex; + } +} + +/** + * Step 3a: Enter environment name (for named environments) + */ +async function enterEnvironmentName(state: CondaCreationState): Promise { + try { + // Try to get a suggested name from project + const suggestedName = getName(state.api, state.uris); + + const name = await showInputBoxWithButtons({ + prompt: CondaStrings.condaNamedInput, + value: suggestedName, + ignoreFocusOut: true, + showBackButton: true, + }); + + if (!name) { + return null; + } + + state.envName = name; + + // Final step - proceed to create environment + return null; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to Python version selection + return selectPythonVersion; + } + throw ex; + } +} + +/** + * Step 3b: Select location (for prefix environments) + */ +async function selectLocation(state: CondaCreationState): Promise { + try { + // Get location using imported getLocation helper + const fsPath = await getLocation(state.api, state.uris || []); + + if (!fsPath) { + return null; + } + + state.fsPath = fsPath; + + // Next step: enter environment name + return enterPrefixName; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to Python version selection + return selectPythonVersion; + } + throw ex; + } +} + +/** + * Step 4: Enter prefix name (for prefix environments) + */ +async function enterPrefixName(state: CondaCreationState): Promise { + try { + if (!state.fsPath) { + return null; + } + + let name = './.conda'; + const defaultPathExists = await fse.pathExists(path.join(state.fsPath, '.conda')); + + // If default name exists, ask for a new name + if (defaultPathExists) { + const newName = await showInputBoxWithButtons({ + prompt: l10n.t('Environment "{0}" already exists. Enter a different name', name), + ignoreFocusOut: true, + showBackButton: true, + validateInput: async (value) => { + // Check if the proposed name already exists + if (!value) { + return l10n.t('Name cannot be empty'); + } + + // Get full path based on input + const fullPath = path.isAbsolute(value) ? value : path.join(state.fsPath!, value); + + // Check if path exists + try { + if (await fse.pathExists(fullPath)) { + return CondaStrings.condaExists; + } + } catch (_) { + // Ignore file system errors during validation + } + + return undefined; + }, + }); + + // If user cancels or presses escape + if (!newName) { + return null; + } + + name = newName; + } + + state.prefix = path.isAbsolute(name) ? name : path.join(state.fsPath, name); + + // Final step - proceed to create environment + return null; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // Go back to location selection + return selectLocation; + } + throw ex; + } +} + +/** + * Main entry point for the step-based Conda environment creation flow. + * + * This function implements a step-based wizard pattern for creating Conda + * environments. This implementation allows users to navigate back to the immediately + * previous step while preserving their selections. + * + * @param api Python Environment API + * @param log Logger for recording operations + * @param manager Environment manager + * @param uris Optional URIs for determining the environment location + * @returns The created environment or undefined if cancelled + */ +export async function createStepBasedCondaFlow( + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + uris?: Uri | Uri[], +): Promise { + // Initialize the state object that will track user selections + const state: CondaCreationState = { + api: api, + uris: Array.isArray(uris) ? uris : uris ? [uris] : [], + }; + + try { + // Start with the first step + let currentStep: StepFunction | null = selectEnvironmentType; + + // Execute steps until completion or cancellation + while (currentStep !== null) { + currentStep = await currentStep(state); + } + + // If we have all required data, create the environment + if (state.envType === CondaStrings.condaNamed && state.envName) { + return await createNamedCondaEnvironment(api, log, manager, state.envName, state.pythonVersion); + } else if (state.envType === CondaStrings.condaPrefix && state.prefix) { + // For prefix environments, we need to pass the fsPath where the environment will be created + return await createPrefixCondaEnvironment(api, log, manager, state.fsPath, state.pythonVersion); + } + + // If we get here, the flow was likely cancelled + return undefined; + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // This should not happen as back navigation is handled within each step + // But if it does, restart the flow + return await createStepBasedCondaFlow(api, log, manager, uris); + } + throw ex; // Re-throw other errors + } +} diff --git a/src/managers/conda/condaUtils.ts b/src/managers/conda/condaUtils.ts index 5fac042..b83be76 100644 --- a/src/managers/conda/condaUtils.ts +++ b/src/managers/conda/condaUtils.ts @@ -1,21 +1,49 @@ import * as ch from 'child_process'; +import * as fse from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; +import { + CancellationError, + CancellationToken, + l10n, + LogOutputChannel, + ProgressLocation, + QuickInputButtons, + QuickPickItem, + ThemeIcon, + Uri, +} from 'vscode'; +import which from 'which'; import { EnvironmentManager, Package, - PackageInfo, - PackageInstallOptions, + PackageManagementOptions, PackageManager, + PythonCommandRunConfiguration, PythonEnvironment, PythonEnvironmentApi, + PythonEnvironmentInfo, PythonProject, } from '../../api'; -import * as path from 'path'; -import * as os from 'os'; -import * as fsapi from 'fs-extra'; -import { LogOutputChannel, ProgressLocation, Uri, window } from 'vscode'; import { ENVS_EXTENSION_ID, EXTENSION_ROOT_DIR } from '../../common/constants'; +import { showErrorMessageWithLogs } from '../../common/errors/utils'; +import { Common, CondaStrings, PackageManagement, Pickers } from '../../common/localize'; +import { traceInfo, traceVerbose } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { pickProject } from '../../common/pickers/projects'; +import { StopWatch } from '../../common/stopWatch'; import { createDeferred } from '../../common/utils/deferred'; -import { pickProject } from '../../common/pickers'; +import { untildify } from '../../common/utils/pathUtils'; +import { isWindows } from '../../common/utils/platformUtils'; +import { + showErrorMessage, + showInputBoxWithButtons, + showQuickPickWithButtons, + withProgress, +} from '../../common/window.apis'; +import { getConfiguration } from '../../common/workspace.apis'; +import { ShellConstants } from '../../features/common/shellConstants'; +import { quoteArgs } from '../../features/execution/execUtils'; import { isNativeEnvInfo, NativeEnvInfo, @@ -23,24 +51,23 @@ import { NativePythonEnvironmentKind, NativePythonFinder, } from '../common/nativePythonFinder'; -import { getConfiguration } from '../../common/workspace.apis'; -import { getGlobalPersistentState, getWorkspacePersistentState } from '../../common/persistentState'; -import which from 'which'; +import { selectFromCommonPackagesToInstall } from '../common/pickers'; +import { Installable } from '../common/types'; import { shortVersion, sortEnvironments } from '../common/utils'; +import { CondaEnvManager } from './condaEnvManager'; +import { getCondaHookPs1Path, getLocalActivationScript } from './condaSourcingUtils'; +import { createStepBasedCondaFlow } from './condaStepBasedFlow'; export const CONDA_PATH_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PATH`; export const CONDA_PREFIXES_KEY = `${ENVS_EXTENSION_ID}:conda:CONDA_PREFIXES`; export const CONDA_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:conda:WORKSPACE_SELECTED`; export const CONDA_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:conda:GLOBAL_SELECTED`; +let condaPath: string | undefined; export async function clearCondaCache(): Promise { - const state = await getWorkspacePersistentState(); - await state.clear([CONDA_PATH_KEY, CONDA_WORKSPACE_KEY, CONDA_GLOBAL_KEY]); - const global = await getGlobalPersistentState(); - await global.clear([CONDA_PREFIXES_KEY]); + condaPath = undefined; } -let condaPath: string | undefined; async function setConda(conda: string): Promise { condaPath = conda; const state = await getWorkspacePersistentState(); @@ -49,7 +76,8 @@ async function setConda(conda: string): Promise { export function getCondaPathSetting(): string | undefined { const config = getConfiguration('python'); - return config.get('condaPath'); + const value = config.get('condaPath'); + return value && typeof value === 'string' ? untildify(value) : value; } export async function getCondaForWorkspace(fsPath: string): Promise { @@ -76,6 +104,19 @@ export async function setCondaForWorkspace(fsPath: string, condaEnvPath: string await state.set(CONDA_WORKSPACE_KEY, data); } +export async function setCondaForWorkspaces(fsPath: string[], condaEnvPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(CONDA_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (condaEnvPath) { + data[s] = condaEnvPath; + } else { + delete data[s]; + } + }); + await state.set(CONDA_WORKSPACE_KEY, data); +} + export async function getCondaForGlobal(): Promise { const state = await getWorkspacePersistentState(); return await state.get(CONDA_GLOBAL_KEY); @@ -94,46 +135,94 @@ async function findConda(): Promise { } } -export async function getConda(): Promise { - const config = getConfiguration('python'); - const conda = config.get('condaPath'); - if (conda) { - return conda; - } - +async function getCondaExecutable(native?: NativePythonFinder): Promise { if (condaPath) { - return condaPath; + if (await fse.pathExists(untildify(condaPath))) { + traceInfo(`Using conda from cache: ${condaPath}`); + return untildify(condaPath); + } } const state = await getWorkspacePersistentState(); condaPath = await state.get(CONDA_PATH_KEY); if (condaPath) { - return condaPath; + if (await fse.pathExists(untildify(condaPath))) { + traceInfo(`Using conda from persistent state: ${condaPath}`); + return untildify(condaPath); + } } const paths = await findConda(); if (paths && paths.length > 0) { - condaPath = paths[0]; - await state.set(CONDA_PATH_KEY, condaPath); - return condaPath; + for (let i = 0; i < paths.length; i++) { + condaPath = paths[i]; + if (await fse.pathExists(untildify(condaPath))) { + traceInfo(`Using conda from PATH: ${condaPath}`); + await state.set(CONDA_PATH_KEY, condaPath); + return condaPath; + } + } + } + + if (native) { + const data = await native.refresh(false); + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'conda'); + if (managers.length > 0) { + for (let i = 0; i < managers.length; i++) { + condaPath = managers[i].executable; + if (await fse.pathExists(untildify(condaPath))) { + traceInfo(`Using conda from native finder: ${condaPath}`); + await state.set(CONDA_PATH_KEY, condaPath); + return condaPath; + } + } + } } throw new Error('Conda not found'); } -async function runConda(args: string[]): Promise { - const conda = await getConda(); +export async function getConda(native?: NativePythonFinder): Promise { + const conda = getCondaPathSetting(); + if (conda) { + traceInfo(`Using conda from settings: ${conda}`); + return conda; + } + + return await getCondaExecutable(native); +} +async function _runConda( + conda: string, + args: string[], + log?: LogOutputChannel, + token?: CancellationToken, +): Promise { const deferred = createDeferred(); + args = quoteArgs(args); + const timer = new StopWatch(); + deferred.promise.finally(() => traceInfo(`Ran conda in ${timer.elapsedTime}: ${conda} ${args.join(' ')}`)); const proc = ch.spawn(conda, args, { shell: true }); + token?.onCancellationRequested(() => { + proc.kill(); + deferred.reject(new CancellationError()); + }); + let stdout = ''; let stderr = ''; proc.stdout?.on('data', (data) => { - stdout += data.toString('utf-8'); + const d = data.toString('utf-8'); + stdout += d; + log?.info(d.trim()); }); proc.stderr?.on('data', (data) => { - stderr += data.toString('utf-8'); + const d = data.toString('utf-8'); + stderr += d; + log?.error(d.trim()); }); proc.on('close', () => { deferred.resolve(stdout); @@ -147,18 +236,33 @@ async function runConda(args: string[]): Promise { return deferred.promise; } +async function runConda(args: string[], log?: LogOutputChannel, token?: CancellationToken): Promise { + const conda = await getConda(); + return await _runConda(conda, args, log, token); +} + +export async function runCondaExecutable( + args: string[], + log?: LogOutputChannel, + token?: CancellationToken, +): Promise { + const conda = await getCondaExecutable(undefined); + return await _runConda(conda, args, log, token); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any async function getCondaInfo(): Promise { const raw = await runConda(['info', '--envs', '--json']); return JSON.parse(raw); } let prefixes: string[] | undefined; -async function getPrefixes(): Promise { +export async function getPrefixes(): Promise { if (prefixes) { return prefixes; } - const state = await getGlobalPersistentState(); + const state = await getWorkspacePersistentState(); prefixes = await state.get(CONDA_PREFIXES_KEY); if (prefixes) { return prefixes; @@ -170,11 +274,16 @@ async function getPrefixes(): Promise { return prefixes; } -async function getVersion(root: string): Promise { - const files = await fsapi.readdir(path.join(root, 'conda-meta')); +export async function getDefaultCondaPrefix(): Promise { + const prefixes = await getPrefixes(); + return prefixes.length > 0 ? prefixes[0] : path.join(os.homedir(), '.conda', 'envs'); +} + +export async function getVersion(root: string): Promise { + const files = await fse.readdir(path.join(root, 'conda-meta')); for (let file of files) { if (file.startsWith('python-3') && file.endsWith('.json')) { - const content = fsapi.readJsonSync(path.join(root, 'conda-meta', file)); + const content = fse.readJsonSync(path.join(root, 'conda-meta', file)); return content['version'] as string; } } @@ -192,63 +301,332 @@ function isPrefixOf(roots: string[], e: string): boolean { return false; } -function nativeToPythonEnv( +/** + * Creates a PythonEnvironmentInfo object for a named conda environment. + * @param name The name of the conda environment + * @param prefix The installation prefix path for the environment + * @param executable The path to the Python executable + * @param version The Python version string + * @param _conda The path to the conda executable (TODO: currently unused) + * @param envManager The environment manager instance + * @returns Promise resolving to a PythonEnvironmentInfo object + */ +export async function getNamedCondaPythonInfo( + name: string, + prefix: string, + executable: string, + version: string, + _conda: string, // TODO:: fix this, why is it not being used to build the info object + envManager: EnvironmentManager, +): Promise { + const { shellActivation, shellDeactivation } = await buildShellActivationMapForConda(prefix, envManager, name); + const sv = shortVersion(version); + + return { + name: name, + environmentPath: Uri.file(prefix), + displayName: `${name} (${sv})`, + shortDisplayName: `${name}:${sv}`, + displayPath: prefix, + description: undefined, + tooltip: prefix, + version: version, + sysPrefix: prefix, + execInfo: { + run: { executable: path.join(executable) }, + activatedRun: { + executable: path.join(executable), + args: [], + }, + activation: [{ executable: 'conda', args: ['activate', name] }], + deactivation: [{ executable: 'conda', args: ['deactivate'] }], + shellActivation, + shellDeactivation, + }, + group: name !== 'base' ? 'Named' : undefined, + }; +} +/** + * Creates a PythonEnvironmentInfo object for a conda environment specified by prefix path. + * @param prefix The installation prefix path for the environment + * @param executable The path to the Python executable + * @param version The Python version string + * @param conda The path to the conda executable + * @param envManager The environment manager instance + * @returns Promise resolving to a PythonEnvironmentInfo object + */ +export async function getPrefixesCondaPythonInfo( + prefix: string, + executable: string, + version: string, + conda: string, + envManager: EnvironmentManager, +): Promise { + const sv = shortVersion(version); + + const { shellActivation, shellDeactivation } = await buildShellActivationMapForConda(prefix, envManager); + + const basename = path.basename(prefix); + return { + name: basename, + environmentPath: Uri.file(prefix), + displayName: `${basename} (${sv})`, + shortDisplayName: `${basename}:${sv}`, + displayPath: prefix, + description: undefined, + tooltip: prefix, + version: version, + sysPrefix: prefix, + execInfo: { + run: { executable: path.join(executable) }, + activatedRun: { + executable: path.join(executable), + args: [], + }, + activation: [{ executable: conda, args: ['activate', prefix] }], + deactivation: [{ executable: conda, args: ['deactivate'] }], + shellActivation, + shellDeactivation, + }, + group: 'Prefix', + }; +} +interface ShellCommandMaps { + shellActivation: Map; + shellDeactivation: Map; +} +/** + * Generates shell-specific activation and deactivation command maps for a conda environment. + * Creates appropriate activation/deactivation commands based on the environment type (named or prefix), + * platform (Windows/non-Windows), and available sourcing scripts. + * + * @param prefix The conda environment prefix path (installation location) + * @param envManager The conda environment manager instance + * @param name Optional name of the conda environment. If provided, used instead of prefix for activation + * @returns Promise resolving to shell-specific activation/deactivation command maps + */ +async function buildShellActivationMapForConda( + prefix: string, + envManager: EnvironmentManager, + name?: string, +): Promise { + const logs: string[] = []; + let shellMaps: ShellCommandMaps; + + try { + // Determine the environment identifier to use + const envIdentifier = name ? name : prefix; + + logs.push(`Environment Configuration: + - Identifier: "${envIdentifier}" + - Prefix: "${prefix}" + - Name: "${name ?? 'undefined'}" +`); + + let condaCommonActivate: PythonCommandRunConfiguration | undefined = { + executable: 'conda', + args: ['activate', envIdentifier], + }; + let condaCommonDeactivate: PythonCommandRunConfiguration | undefined = { + executable: 'conda', + args: ['deactivate'], + }; + + if (!(envManager instanceof CondaEnvManager) || !envManager.sourcingInformation) { + logs.push('Error: Conda environment manager is not available, using default conda activation paths'); + shellMaps = await generateShellActivationMapFromConfig([condaCommonActivate], [condaCommonDeactivate]); + return shellMaps; + } + + const { isActiveOnLaunch, globalSourcingScript } = envManager.sourcingInformation; + + // P1: first check to see if conda is already active in the whole VS Code workspace via sourcing info (set at startup) + if (isActiveOnLaunch) { + logs.push('✓ Conda already active on launch, using default activation commands'); + shellMaps = await generateShellActivationMapFromConfig([condaCommonActivate], [condaCommonDeactivate]); + return shellMaps; + } + + // get the local activation path, if exists use this + let localSourcingPath: string | undefined; + try { + localSourcingPath = await getLocalActivationScript(prefix); + } catch (err) { + logs.push(`Error getting local activation script: ${err instanceof Error ? err.message : 'Unknown error'}`); + } + + logs.push(`Local Activation: + - Status: ${localSourcingPath ? 'Found' : 'Not Found'} + - Path: ${localSourcingPath ?? 'N/A'} +`); + + // Determine the preferred sourcing path with preference to local + const preferredSourcingPath = localSourcingPath || globalSourcingScript; + logs.push(`Preferred Sourcing: + - Selected Path: ${preferredSourcingPath ?? 'none found'} + - Source: ${localSourcingPath ? 'Local' : globalSourcingScript ? 'Global' : 'None'} +`); + + // P2: Return shell activation if we have no sourcing + if (!preferredSourcingPath) { + logs.push('No sourcing path found, using default conda activation'); + shellMaps = await generateShellActivationMapFromConfig([condaCommonActivate], [condaCommonDeactivate]); + return shellMaps; + } + + // P3: Handle Windows specifically ;this is carryover from vscode-python + if (isWindows()) { + logs.push('✓ Using Windows-specific activation configuration'); + shellMaps = await windowsExceptionGenerateConfig( + preferredSourcingPath, + envIdentifier, + envManager.sourcingInformation.condaFolder, + ); + return shellMaps; + } + + logs.push('✓ Using source command with preferred path'); + const condaSourcingPathFirst = { + executable: 'source', + args: [preferredSourcingPath, envIdentifier], + }; + shellMaps = await generateShellActivationMapFromConfig([condaSourcingPathFirst], [condaCommonDeactivate]); + return shellMaps; + } catch (error) { + logs.push( + `Error in shell activation map generation: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); + // Fall back to default conda activation in case of error + shellMaps = await generateShellActivationMapFromConfig( + [{ executable: 'conda', args: ['activate', name || prefix] }], + [{ executable: 'conda', args: ['deactivate'] }], + ); + return shellMaps; + } finally { + // Always print logs in a nicely formatted block, even if there was an error + traceInfo( + [ + '=== Conda Shell Activation Map Generation ===', + ...logs, + '==========================================', + ].join('\n'), + ); + } +} + +async function generateShellActivationMapFromConfig( + activate: PythonCommandRunConfiguration[], + deactivate: PythonCommandRunConfiguration[], +): Promise { + const shellActivation: Map = new Map(); + const shellDeactivation: Map = new Map(); + shellActivation.set(ShellConstants.GITBASH, activate); + shellDeactivation.set(ShellConstants.GITBASH, deactivate); + + shellActivation.set(ShellConstants.CMD, activate); + shellDeactivation.set(ShellConstants.CMD, deactivate); + + shellActivation.set(ShellConstants.BASH, activate); + shellDeactivation.set(ShellConstants.BASH, deactivate); + + shellActivation.set(ShellConstants.SH, activate); + shellDeactivation.set(ShellConstants.SH, deactivate); + + shellActivation.set(ShellConstants.ZSH, activate); + shellDeactivation.set(ShellConstants.ZSH, deactivate); + + shellActivation.set(ShellConstants.PWSH, activate); + shellDeactivation.set(ShellConstants.PWSH, deactivate); + + return { shellActivation, shellDeactivation }; +} + +async function windowsExceptionGenerateConfig( + sourceInitPath: string, + prefix: string, + condaFolder: string, +): Promise { + const shellActivation: Map = new Map(); + const shellDeactivation: Map = new Map(); + + const ps1Hook = await getCondaHookPs1Path(condaFolder); + traceVerbose(`PS1 hook path: ${ps1Hook ?? 'not found'}`); + const activation = ps1Hook ? ps1Hook : sourceInitPath; + + const pwshActivate = [{ executable: activation }, { executable: 'conda', args: ['activate', prefix] }]; + const cmdActivate = [{ executable: sourceInitPath }, { executable: 'conda', args: ['activate', prefix] }]; + + const bashActivate = [{ executable: 'source', args: [sourceInitPath.replace(/\\/g, '/'), prefix] }]; + traceVerbose( + `Windows activation commands: + PowerShell: ${JSON.stringify(pwshActivate)}, + CMD: ${JSON.stringify(cmdActivate)}, + Bash: ${JSON.stringify(bashActivate)}`, + ); + + let condaCommonDeactivate: PythonCommandRunConfiguration | undefined = { + executable: 'conda', + args: ['deactivate'], + }; + shellActivation.set(ShellConstants.GITBASH, bashActivate); + shellDeactivation.set(ShellConstants.GITBASH, [condaCommonDeactivate]); + + shellActivation.set(ShellConstants.CMD, cmdActivate); + shellDeactivation.set(ShellConstants.CMD, [condaCommonDeactivate]); + + shellActivation.set(ShellConstants.PWSH, pwshActivate); + shellDeactivation.set(ShellConstants.PWSH, [condaCommonDeactivate]); + + return { shellActivation, shellDeactivation }; +} + +function getCondaWithoutPython(name: string, prefix: string, conda: string): PythonEnvironmentInfo { + return { + name: name, + environmentPath: Uri.file(prefix), + displayName: `${name} (no-python)`, + shortDisplayName: `${name} (no-python)`, + displayPath: prefix, + description: prefix, + tooltip: l10n.t('Conda environment without Python'), + version: 'no-python', + sysPrefix: prefix, + iconPath: new ThemeIcon('stop'), + execInfo: { + run: { executable: conda }, + }, + group: name.length > 0 ? 'Named' : 'Prefix', + }; +} + +async function nativeToPythonEnv( e: NativeEnvInfo, api: PythonEnvironmentApi, manager: EnvironmentManager, log: LogOutputChannel, conda: string, condaPrefixes: string[], -): PythonEnvironment | undefined { +): Promise { if (!(e.prefix && e.executable && e.version)) { - log.warn(`Invalid conda environment: ${JSON.stringify(e)}`); - return; + let name = e.name; + const environment = api.createPythonEnvironmentItem( + getCondaWithoutPython(name ?? '', e.prefix ?? '', conda), + manager, + ); + log.info(`Found a No-Python conda environment: ${e.executable ?? e.prefix ?? 'conda-no-python'}`); + return environment; } - const sv = shortVersion(e.version); + if (e.name === 'base') { const environment = api.createPythonEnvironmentItem( - { - name: 'base', - environmentPath: Uri.file(e.prefix), - displayName: `base (${sv})`, - shortDisplayName: `base:${sv}`, - displayPath: e.name, - description: e.prefix, - version: e.version, - sysPrefix: e.prefix, - execInfo: { - run: { executable: path.join(e.executable) }, - activatedRun: { executable: conda, args: ['run', '--live-stream', '--name', 'base', 'python'] }, - activation: [{ executable: conda, args: ['activate', 'base'] }], - deactivation: [{ executable: conda, args: ['deactivate'] }], - }, - }, + await getNamedCondaPythonInfo('base', e.prefix, e.executable, e.version, conda, manager), manager, ); log.info(`Found base environment: ${e.prefix}`); return environment; } else if (!isPrefixOf(condaPrefixes, e.prefix)) { - const basename = path.basename(e.prefix); const environment = api.createPythonEnvironmentItem( - { - name: basename, - environmentPath: Uri.file(e.prefix), - displayName: `${basename} (${sv})`, - shortDisplayName: `${basename}:${sv}`, - displayPath: e.prefix, - description: e.prefix, - version: e.version, - sysPrefix: e.prefix, - execInfo: { - run: { executable: path.join(e.executable) }, - activatedRun: { - executable: conda, - args: ['run', '--live-stream', '--prefix', e.prefix, 'python'], - }, - activation: [{ executable: conda, args: ['activate', e.prefix] }], - deactivation: [{ executable: conda, args: ['deactivate'] }], - }, - }, + await getPrefixesCondaPythonInfo(e.prefix, e.executable, e.version, conda, manager), manager, ); log.info(`Found prefix environment: ${e.prefix}`); @@ -257,25 +635,7 @@ function nativeToPythonEnv( const basename = path.basename(e.prefix); const name = e.name ?? basename; const environment = api.createPythonEnvironmentItem( - { - name: name, - environmentPath: Uri.file(e.prefix), - displayName: `${name} (${sv})`, - shortDisplayName: `${name}:${sv}`, - displayPath: e.prefix, - description: e.prefix, - version: e.version, - sysPrefix: e.prefix, - execInfo: { - run: { executable: path.join(e.executable) }, - activatedRun: { - executable: conda, - args: ['run', '--live-stream', '--name', name, 'python'], - }, - activation: [{ executable: conda, args: ['activate', name] }], - deactivation: [{ executable: conda, args: ['deactivate'] }], - }, - }, + await getNamedCondaPythonInfo(name, e.prefix, e.executable, e.version, conda, manager), manager, ); log.info(`Found named environment: ${e.prefix}`); @@ -338,12 +698,14 @@ export async function refreshCondaEnvs( .filter((e) => e.kind === NativePythonEnvironmentKind.conda); const collection: PythonEnvironment[] = []; - envs.forEach((e) => { - const environment = nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); - if (environment) { - collection.push(environment); - } - }); + await Promise.all( + envs.map(async (e) => { + const environment = await nativeToPythonEnv(e, api, manager, log, condaPath, condaPrefixes); + if (environment) { + collection.push(environment); + } + }), + ); return sortEnvironments(collection); } @@ -352,137 +714,295 @@ export async function refreshCondaEnvs( return []; } -export async function createCondaEnvironment( +export function getName(api: PythonEnvironmentApi, uris?: Uri | Uri[]): string | undefined { + if (!uris) { + return undefined; + } + if (Array.isArray(uris) && uris.length !== 1) { + return undefined; + } + return api.getPythonProject(Array.isArray(uris) ? uris[0] : uris)?.name; +} + +export async function getLocation(api: PythonEnvironmentApi, uris: Uri | Uri[]): Promise { + if (!uris || (Array.isArray(uris) && (uris.length === 0 || uris.length > 1))) { + const projects: PythonProject[] = []; + if (Array.isArray(uris)) { + for (let uri of uris) { + const project = api.getPythonProject(uri); + if (project && !projects.includes(project)) { + projects.push(project); + } + } + } else { + api.getPythonProjects().forEach((p) => projects.push(p)); + } + const project = await pickProject(projects); + return project?.uri.fsPath; + } + return api.getPythonProject(Array.isArray(uris) ? uris[0] : uris)?.uri.fsPath; +} +const RECOMMENDED_CONDA_PYTHON = '3.11.11'; + +export function trimVersionToMajorMinor(version: string): string { + const match = version.match(/^(\d+\.\d+\.\d+)/); + return match ? match[1] : version; +} +export async function pickPythonVersion( api: PythonEnvironmentApi, - log: LogOutputChannel, - manager: EnvironmentManager, - project?: PythonProject, -): Promise { - // step1 ask user for named or prefix environment - const envType = await window.showQuickPick( - [ - { label: 'Named', description: 'Create a named conda environment' }, - { label: 'Prefix', description: 'Create environment in your workspace' }, - ], + token?: CancellationToken, +): Promise { + const envs = await api.getEnvironments('global'); + let versions = Array.from( + new Set( + envs + .map((env) => env.version) + .filter(Boolean) + .map((v) => trimVersionToMajorMinor(v)), // cut to 3 digits + ), + ); + + // Sort versions by major version (descending), ignoring minor/patch for simplicity + const parseMajorMinor = (v: string) => { + const m = v.match(/^(\d+)(?:\.(\d+))?/); + return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 }; + }; + + versions = versions.sort((a, b) => { + const pa = parseMajorMinor(a); + const pb = parseMajorMinor(b); + if (pa.major !== pb.major) { + return pb.major - pa.major; + } // desc by major + return pb.minor - pa.minor; // desc by minor + }); + + if (!versions || versions.length === 0) { + versions = ['3.13', '3.12', '3.11', '3.10', '3.9']; + } + const items: QuickPickItem[] = versions.map((v) => ({ + label: v === RECOMMENDED_CONDA_PYTHON ? `$(star-full) Python` : 'Python', + description: v, + })); + const selection = await showQuickPickWithButtons( + items, { - placeHolder: 'Select the type of conda environment to create', + placeHolder: l10n.t('Select the version of Python to install in the environment'), + matchOnDescription: true, ignoreFocusOut: true, + showBackButton: true, }, + token, ); - if (envType) { - return envType.label === 'Named' - ? await createNamedCondaEnvironment(api, log, manager, project) - : await createPrefixCondaEnvironment(api, log, manager, project); + if (selection) { + return (selection as QuickPickItem).description; } + return undefined; } -async function createNamedCondaEnvironment( +export async function createCondaEnvironment( api: PythonEnvironmentApi, log: LogOutputChannel, manager: EnvironmentManager, - project?: PythonProject, + uris?: Uri | Uri[], ): Promise { - const name = await window.showInputBox({ - prompt: 'Enter the name of the conda environment to create', - value: project?.name, - ignoreFocusOut: true, - }); - if (!name) { - return; + return createStepBasedCondaFlow(api, log, manager, uris); +} + +export async function createNamedCondaEnvironment( + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + name?: string, + pythonVersion?: string, +): Promise { + try { + name = await showInputBoxWithButtons({ + prompt: CondaStrings.condaNamedInput, + value: name, + ignoreFocusOut: true, + showBackButton: true, + }); + if (!name) { + return; + } + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // If back button was pressed, go back to the environment type selection + return await createCondaEnvironment(api, log, manager); + } + throw ex; } - return await window.withProgress( + const envName: string = name; + const runArgs = ['create', '--yes', '--name', envName]; + if (pythonVersion) { + runArgs.push(`python=${pythonVersion}`); + } else { + runArgs.push('python'); + } + + return await withProgress( { location: ProgressLocation.Notification, - title: `Creating conda environment: ${name}`, + title: l10n.t('Creating conda environment: {0}', envName), }, async () => { try { const bin = os.platform() === 'win32' ? 'python.exe' : 'python'; - const output = await runConda(['create', '--yes', '--name', name, 'python']); + const output = await runCondaExecutable(runArgs); log.info(output); const prefixes = await getPrefixes(); let envPath = ''; for (let prefix of prefixes) { - if (await fsapi.pathExists(path.join(prefix, name))) { - envPath = path.join(prefix, name); + if (await fse.pathExists(path.join(prefix, envName))) { + envPath = path.join(prefix, envName); break; } } const version = await getVersion(envPath); const environment = api.createPythonEnvironmentItem( - { - name, - environmentPath: Uri.file(envPath), - displayName: `${version} (${name})`, - displayPath: envPath, - description: envPath, + await getNamedCondaPythonInfo( + envName, + envPath, + path.join(envPath, bin), version, - execInfo: { - activatedRun: { executable: 'conda', args: ['run', '--live-stream', '-n', name, 'python'] }, - activation: [{ executable: 'conda', args: ['activate', name] }], - deactivation: [{ executable: 'conda', args: ['deactivate'] }], - run: { executable: path.join(envPath, bin) }, - }, - sysPrefix: envPath, - }, + await getConda(), + manager, + ), manager, ); return environment; } catch (e) { - window.showErrorMessage('Failed to create conda environment'); log.error('Failed to create conda environment', e); + setImmediate(async () => { + await showErrorMessageWithLogs(CondaStrings.condaCreateFailed, log); + }); } }, ); } -async function createPrefixCondaEnvironment( +export async function createPrefixCondaEnvironment( api: PythonEnvironmentApi, log: LogOutputChannel, manager: EnvironmentManager, - project?: PythonProject, + fsPath?: string, + pythonVersion?: string, ): Promise { - const picked = project?.uri.fsPath ?? (await pickProject(api.getPythonProjects()))?.uri.fsPath; - if (!picked) { - return; - } + try { + if (!fsPath) { + return; + } - let name = `./.conda`; - if (await fsapi.pathExists(path.join(picked, '.conda'))) { - log.warn(`Environment "${path.join(picked, '.conda')}" already exists`); - const newName = await window.showInputBox({ - prompt: `Environment "${name}" already exists. Enter a different name`, - ignoreFocusOut: true, - validateInput: (value) => { - if (value === name) { - return 'Environment already exists'; + let name = `./.conda`; + if (await fse.pathExists(path.join(fsPath, '.conda'))) { + log.warn(`Environment "${path.join(fsPath, '.conda')}" already exists`); + const newName = await showInputBoxWithButtons({ + prompt: l10n.t('Environment "{0}" already exists. Enter a different name', name), + ignoreFocusOut: true, + showBackButton: true, + validateInput: (value) => { + if (value === name) { + return CondaStrings.condaExists; + } + return undefined; + }, + }); + if (!newName) { + return; + } + name = newName; + } + + const prefix: string = path.isAbsolute(name) ? name : path.join(fsPath, name); + + const runArgs = ['create', '--yes', '--prefix', prefix]; + if (pythonVersion) { + runArgs.push(`python=${pythonVersion}`); + } else { + runArgs.push('python'); + } + + return await withProgress( + { + location: ProgressLocation.Notification, + title: `Creating conda environment: ${name}`, + }, + async () => { + try { + const bin = os.platform() === 'win32' ? 'python.exe' : 'python'; + const output = await runCondaExecutable(runArgs); + log.info(output); + const version = await getVersion(prefix); + + const environment = api.createPythonEnvironmentItem( + await getPrefixesCondaPythonInfo( + prefix, + path.join(prefix, bin), + version, + await getConda(), + manager, + ), + manager, + ); + return environment; + } catch (e) { + log.error('Failed to create conda environment', e); + setImmediate(async () => { + await showErrorMessageWithLogs(CondaStrings.condaCreateFailed, log); + }); } - return undefined; }, - }); - if (!newName) { - return; + ); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + // If back button was pressed, go back to the environment type selection + return await createCondaEnvironment(api, log, manager); } - name = newName; + throw ex; } +} - const prefix: string = path.isAbsolute(name) ? name : path.join(picked, name); +export async function generateName(fsPath: string): Promise { + let attempts = 0; + while (attempts < 5) { + const randomStr = Math.random().toString(36).substring(2); + const name = `env_${randomStr}`; + const prefix = path.join(fsPath, name); + if (!(await fse.exists(prefix))) { + return name; + } + } + return undefined; +} - return await window.withProgress( +export async function quickCreateConda( + api: PythonEnvironmentApi, + log: LogOutputChannel, + manager: EnvironmentManager, + fsPath: string, + name: string, + additionalPackages?: string[], +): Promise { + const prefix = path.join(fsPath, name); + const execPath = os.platform() === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'bin', 'python'); + + return await withProgress( { location: ProgressLocation.Notification, title: `Creating conda environment: ${name}`, }, async () => { try { - const bin = os.platform() === 'win32' ? 'python.exe' : 'python'; - const output = await runConda(['create', '--yes', '--prefix', prefix, 'python']); - log.info(output); + await runCondaExecutable(['create', '--yes', '--prefix', prefix, 'python'], log); + if (additionalPackages && additionalPackages.length > 0) { + await runConda(['install', '--yes', '--prefix', prefix, ...additionalPackages], log); + } const version = await getVersion(prefix); const environment = api.createPythonEnvironmentItem( @@ -494,22 +1014,25 @@ async function createPrefixCondaEnvironment( description: prefix, version, execInfo: { - run: { executable: path.join(prefix, bin) }, + run: { executable: execPath }, activatedRun: { - executable: 'conda', - args: ['run', '--live-stream', '-p', prefix, 'python'], + executable: execPath, + args: [], }, activation: [{ executable: 'conda', args: ['activate', prefix] }], deactivation: [{ executable: 'conda', args: ['deactivate'] }], }, sysPrefix: prefix, + group: 'Prefix', }, manager, ); return environment; } catch (e) { - window.showErrorMessage('Failed to create conda environment'); log.error('Failed to create conda environment', e); + setImmediate(async () => { + await showErrorMessageWithLogs(CondaStrings.condaCreateFailed, log); + }); } }, ); @@ -517,17 +1040,19 @@ async function createPrefixCondaEnvironment( export async function deleteCondaEnvironment(environment: PythonEnvironment, log: LogOutputChannel): Promise { let args = ['env', 'remove', '--yes', '--prefix', environment.environmentPath.fsPath]; - return await window.withProgress( + return await withProgress( { location: ProgressLocation.Notification, - title: `Deleting conda environment: ${environment.environmentPath.fsPath}`, + title: l10n.t('Deleting conda environment: {0}', environment.environmentPath.fsPath), }, async () => { try { - await runConda(args); + await runCondaExecutable(args, log); } catch (e) { - window.showErrorMessage('Failed to delete conda environment'); log.error(`Failed to delete conda environment: ${e}`); + setImmediate(async () => { + await showErrorMessageWithLogs(CondaStrings.condaRemoveFailed, log); + }); return false; } return true; @@ -541,18 +1066,18 @@ export async function refreshPackages( manager: PackageManager, ): Promise { let args = ['list', '-p', environment.environmentPath.fsPath]; - const data = await runConda(args); + const data = await runCondaExecutable(args); const content = data.split(/\r?\n/).filter((l) => !l.startsWith('#')); const packages: Package[] = []; content.forEach((l) => { const parts = l.split(' ').filter((p) => p.length > 0); - if (parts.length === 3) { + if (parts.length >= 3) { const pkg = api.createPackageItem( { name: parts[0], displayName: parts[0], version: parts[1], - description: parts[2], + description: parts[1], }, environment, manager, @@ -563,47 +1088,164 @@ export async function refreshPackages( return packages; } -export async function installPackages( +export async function managePackages( environment: PythonEnvironment, - packages: string[], - options: PackageInstallOptions, + options: PackageManagementOptions, api: PythonEnvironmentApi, manager: PackageManager, + token: CancellationToken, + log: LogOutputChannel, ): Promise { - if (!packages || packages.length === 0) { - // TODO: Ask user to pick packages - throw new Error('No packages to install'); + if (options.uninstall && options.uninstall.length > 0) { + await runCondaExecutable( + ['remove', '--prefix', environment.environmentPath.fsPath, '--yes', ...options.uninstall], + log, + token, + ); + } + if (options.install && options.install.length > 0) { + const args = ['install', '--prefix', environment.environmentPath.fsPath, '--yes']; + if (options.upgrade) { + args.push('--update-all'); + } + args.push(...options.install); + await runCondaExecutable(args, log, token); } + return refreshPackages(environment, api, manager); +} - const args = ['install', '--prefix', environment.environmentPath.fsPath, '--yes']; - if (options.upgrade) { - args.push('--update-all'); +async function getCommonPackages(): Promise { + try { + const pipData = path.join(EXTENSION_ROOT_DIR, 'files', 'conda_packages.json'); + const data = await fse.readFile(pipData, { encoding: 'utf-8' }); + const packages = JSON.parse(data) as { name: string; description: string; uri: string }[]; + + return packages.map((p) => { + return { + name: p.name, + displayName: p.name, + uri: Uri.parse(p.uri), + description: p.description, + }; + }); + } catch { + return []; } - args.push(...packages); +} - await runConda(args); - return refreshPackages(environment, api, manager); +interface CondaPackagesResult { + install: string[]; + uninstall: string[]; } -export async function uninstallPackages( +async function selectCommonPackagesOrSkip( + common: Installable[], + installed: string[], + showSkipOption: boolean, +): Promise { + if (common.length === 0) { + return undefined; + } + + const items: QuickPickItem[] = []; + if (common.length > 0) { + items.push({ + label: PackageManagement.searchCommonPackages, + description: PackageManagement.searchCommonPackagesDescription, + }); + } + + if (showSkipOption && items.length > 0) { + items.push({ label: PackageManagement.skipPackageInstallation }); + } + + let showBackButton = true; + let selected: QuickPickItem[] | QuickPickItem | undefined = undefined; + if (items.length === 1) { + selected = items[0]; + showBackButton = false; + } else { + selected = await showQuickPickWithButtons(items, { + placeHolder: Pickers.Packages.selectOption, + ignoreFocusOut: true, + showBackButton: true, + matchOnDescription: false, + matchOnDetail: false, + }); + } + + if (selected && !Array.isArray(selected)) { + try { + if (selected.label === PackageManagement.searchCommonPackages) { + return await selectFromCommonPackagesToInstall(common, installed, undefined, { showBackButton }); + } else { + traceInfo('Package Installer: user selected skip package installation'); + return undefined; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (ex: any) { + if (ex === QuickInputButtons.Back) { + return selectCommonPackagesOrSkip(common, installed, showSkipOption); + } + } + } + return undefined; +} + +export async function getCommonCondaPackagesToInstall( environment: PythonEnvironment, - packages: PackageInfo[] | string[], + options: PackageManagementOptions, api: PythonEnvironmentApi, - manager: PackageManager, -): Promise { - const remove = []; - for (let pkg of packages) { - if (typeof pkg === 'string') { - remove.push(pkg); - } else { - remove.push(pkg.name); - } +): Promise { + const common = await getCommonPackages(); + const installed = (await api.getPackages(environment))?.map((p) => p.name); + const selected = await selectCommonPackagesOrSkip(common, installed ?? [], !!options.showSkipOption); + return selected; +} + +async function installPython( + nativeFinder: NativePythonFinder, + manager: EnvironmentManager, + environment: PythonEnvironment, + api: PythonEnvironmentApi, + log: LogOutputChannel, +): Promise { + if (environment.sysPrefix === '') { + return undefined; } - if (remove.length === 0) { - throw new Error('No packages to remove'); + await runCondaExecutable(['install', '--yes', '--prefix', environment.sysPrefix, 'python'], log); + await nativeFinder.refresh(true, NativePythonEnvironmentKind.conda); + const native = await nativeFinder.resolve(environment.sysPrefix); + if (native.kind === NativePythonEnvironmentKind.conda) { + return nativeToPythonEnv(native, api, manager, log, await getConda(), await getPrefixes()); } + return undefined; +} - await runConda(['remove', '--prefix', environment.environmentPath.fsPath, '--yes', ...remove]); - - return refreshPackages(environment, api, manager); +export async function checkForNoPythonCondaEnvironment( + nativeFinder: NativePythonFinder, + manager: EnvironmentManager, + environment: PythonEnvironment, + api: PythonEnvironmentApi, + log: LogOutputChannel, +): Promise { + if (environment.version === 'no-python') { + if (environment.sysPrefix === '') { + await showErrorMessage(CondaStrings.condaMissingPythonNoFix, { modal: true }); + return undefined; + } else { + const result = await showErrorMessage( + `${CondaStrings.condaMissingPython}: ${environment.displayName}`, + { + modal: true, + }, + Common.installPython, + ); + if (result === Common.installPython) { + return await installPython(nativeFinder, manager, environment, api, log); + } + return undefined; + } + } + return environment; } diff --git a/src/managers/conda/main.ts b/src/managers/conda/main.ts index e62dae3..70456ef 100644 --- a/src/managers/conda/main.ts +++ b/src/managers/conda/main.ts @@ -1,24 +1,34 @@ import { Disposable, LogOutputChannel } from 'vscode'; import { PythonEnvironmentApi } from '../../api'; -import { CondaEnvManager } from './condaEnvManager'; -import { CondaPackageManager } from './condaPackageManager'; +import { traceInfo } from '../../common/logging'; import { getPythonApi } from '../../features/pythonApi'; +import { PythonProjectManager } from '../../internal.api'; import { NativePythonFinder } from '../common/nativePythonFinder'; -import { traceInfo } from '../../common/logging'; +import { notifyMissingManagerIfDefault } from '../common/utils'; +import { CondaEnvManager } from './condaEnvManager'; +import { CondaPackageManager } from './condaPackageManager'; +import { CondaSourcingStatus, constructCondaSourcingStatus } from './condaSourcingUtils'; import { getConda } from './condaUtils'; -import { onDidChangeConfiguration } from '../../common/workspace.apis'; -async function register( - api: PythonEnvironmentApi, +export async function registerCondaFeatures( nativeFinder: NativePythonFinder, + disposables: Disposable[], log: LogOutputChannel, -): Promise { - const disposables: Disposable[] = []; + projectManager: PythonProjectManager, +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + try { - await getConda(); + // get Conda will return only ONE conda manager, that correlates to a single conda install + const condaPath: string = await getConda(nativeFinder); + const sourcingStatus: CondaSourcingStatus = await constructCondaSourcingStatus(condaPath); + traceInfo(sourcingStatus.toString()); + const envManager = new CondaEnvManager(nativeFinder, api, log); const packageManager = new CondaPackageManager(api, log); + envManager.sourcingInformation = sourcingStatus; + disposables.push( envManager, packageManager, @@ -26,26 +36,7 @@ async function register( api.registerPackageManager(packageManager), ); } catch (ex) { - traceInfo('Conda not found, turning off conda features.'); + traceInfo('Conda not found, turning off conda features.', ex); + await notifyMissingManagerIfDefault('ms-python.python:conda', projectManager, api); } - return Disposable.from(...disposables); -} - -export async function registerCondaFeatures( - nativeFinder: NativePythonFinder, - disposables: Disposable[], - log: LogOutputChannel, -): Promise { - const api: PythonEnvironmentApi = await getPythonApi(); - - const disposable: Disposable = await register(api, nativeFinder, log); - - disposables.push( - disposable, - onDidChangeConfiguration((e) => { - if (e.affectsConfiguration('python.condaPath')) { - // TODO: This setting requires a reload of the extension. - } - }), - ); } diff --git a/src/managers/pipenv/main.ts b/src/managers/pipenv/main.ts new file mode 100644 index 0000000..b84094d --- /dev/null +++ b/src/managers/pipenv/main.ts @@ -0,0 +1,33 @@ +import { Disposable } from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { traceInfo } from '../../common/logging'; +import { getPythonApi } from '../../features/pythonApi'; +import { PythonProjectManager } from '../../internal.api'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { PipenvManager } from './pipenvManager'; +import { getPipenv } from './pipenvUtils'; + +import { notifyMissingManagerIfDefault } from '../common/utils'; + +export async function registerPipenvFeatures( + nativeFinder: NativePythonFinder, + disposables: Disposable[], + projectManager: PythonProjectManager, +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + + try { + const pipenv = await getPipenv(nativeFinder); + + if (pipenv) { + const mgr = new PipenvManager(nativeFinder, api); + disposables.push(mgr, api.registerEnvironmentManager(mgr)); + } else { + traceInfo('Pipenv not found, turning off pipenv features.'); + await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); + } + } catch (ex) { + traceInfo('Pipenv not found, turning off pipenv features.', ex); + await notifyMissingManagerIfDefault('ms-python.python:pipenv', projectManager, api); + } +} diff --git a/src/managers/pipenv/pipenvManager.ts b/src/managers/pipenv/pipenvManager.ts new file mode 100644 index 0000000..3caba8b --- /dev/null +++ b/src/managers/pipenv/pipenvManager.ts @@ -0,0 +1,274 @@ +import { EventEmitter, MarkdownString, ProgressLocation, Uri } from 'vscode'; +import { + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + PythonEnvironmentApi, + PythonProject, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../../api'; +import { PipenvStrings } from '../../common/localize'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { withProgress } from '../../common/window.apis'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { + clearPipenvCache, + getPipenvForGlobal, + getPipenvForWorkspace, + refreshPipenv, + resolvePipenvPath, + setPipenvForGlobal, + setPipenvForWorkspace, + setPipenvForWorkspaces, +} from './pipenvUtils'; + +export class PipenvManager implements EnvironmentManager { + private collection: PythonEnvironment[] = []; + private fsPathToEnv: Map = new Map(); + private globalEnv: PythonEnvironment | undefined; + + private readonly _onDidChangeEnvironment = new EventEmitter(); + public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; + + private readonly _onDidChangeEnvironments = new EventEmitter(); + public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + + public readonly name: string; + public readonly displayName: string; + public readonly preferredPackageManagerId: string; + public readonly description?: string; + public readonly tooltip: string | MarkdownString; + public readonly iconPath?: IconPath; + + private _initialized: Deferred | undefined; + + constructor(public readonly nativeFinder: NativePythonFinder, public readonly api: PythonEnvironmentApi) { + this.name = 'pipenv'; + this.displayName = 'Pipenv'; + this.preferredPackageManagerId = 'ms-python.python:pip'; + this.tooltip = new MarkdownString(PipenvStrings.pipenvManager, true); + } + + public dispose() { + this.collection = []; + this.fsPathToEnv.clear(); + this._onDidChangeEnvironment.dispose(); + this._onDidChangeEnvironments.dispose(); + } + + async initialize(): Promise { + if (this._initialized) { + return this._initialized.promise; + } + + this._initialized = createDeferred(); + + await withProgress( + { + location: ProgressLocation.Window, + title: PipenvStrings.pipenvDiscovering, + }, + async () => { + this.collection = await refreshPipenv(false, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + this._onDidChangeEnvironments.fire( + this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })), + ); + }, + ); + this._initialized.resolve(); + } + + private async loadEnvMap() { + // Load environment mappings for projects + const projects = this.api.getPythonProjects(); + for (const project of projects) { + const envPath = await getPipenvForWorkspace(project.uri.fsPath); + if (envPath) { + const env = this.findEnvironmentByPath(envPath); + if (env) { + this.fsPathToEnv.set(project.uri.fsPath, env); + } + } + } + + // Load global environment + const globalEnvPath = await getPipenvForGlobal(); + if (globalEnvPath) { + this.globalEnv = this.findEnvironmentByPath(globalEnvPath); + } + } + + private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { + return this.collection.find( + (env) => env.environmentPath.fsPath === fsPath || env.execInfo?.run.executable === fsPath, + ); + } + + async refresh(scope: RefreshEnvironmentsScope): Promise { + const hardRefresh = scope === undefined; // hard refresh when scope is undefined + + await withProgress( + { + location: ProgressLocation.Window, + title: PipenvStrings.pipenvRefreshing, + }, + async () => { + const oldCollection = [...this.collection]; + this.collection = await refreshPipenv(hardRefresh, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + // Fire change events for environments that were added or removed + const changes: { environment: PythonEnvironment; kind: EnvironmentChangeKind }[] = []; + + // Find removed environments + oldCollection.forEach((oldEnv) => { + if (!this.collection.find((newEnv) => newEnv.envId.id === oldEnv.envId.id)) { + changes.push({ environment: oldEnv, kind: EnvironmentChangeKind.remove }); + } + }); + + // Find added environments + this.collection.forEach((newEnv) => { + if (!oldCollection.find((oldEnv) => oldEnv.envId.id === newEnv.envId.id)) { + changes.push({ environment: newEnv, kind: EnvironmentChangeKind.add }); + } + }); + + if (changes.length > 0) { + this._onDidChangeEnvironments.fire(changes); + } + }, + ); + } + + async getEnvironments(scope: GetEnvironmentsScope): Promise { + await this.initialize(); + + if (scope === 'all') { + return Array.from(this.collection); + } + + if (scope === 'global') { + // Return all environments for global scope + return Array.from(this.collection); + } + + if (scope instanceof Uri) { + const project = this.api.getPythonProject(scope); + if (project) { + const env = this.fsPathToEnv.get(project.uri.fsPath); + return env ? [env] : []; + } + } + + return []; + } + + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { + if (scope === undefined) { + // Global scope + const before = this.globalEnv; + this.globalEnv = environment; + await setPipenvForGlobal(environment?.environmentPath.fsPath); + + if (before?.envId.id !== this.globalEnv?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: undefined, old: before, new: this.globalEnv }); + } + return; + } + + if (scope instanceof Uri) { + // Single project scope + const project = this.api.getPythonProject(scope); + if (!project) { + return; + } + + const before = this.fsPathToEnv.get(project.uri.fsPath); + if (environment) { + this.fsPathToEnv.set(project.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(project.uri.fsPath); + } + + await setPipenvForWorkspace(project.uri.fsPath, environment?.environmentPath.fsPath); + + if (before?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment }); + } + } + + if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + // Multiple projects scope + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setPipenvForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); + } + } + + async get(scope: GetEnvironmentScope): Promise { + await this.initialize(); + + if (scope === undefined) { + return this.globalEnv; + } + + if (scope instanceof Uri) { + const project = this.api.getPythonProject(scope); + if (project) { + return this.fsPathToEnv.get(project.uri.fsPath); + } + } + + return undefined; + } + + async resolve(context: ResolveEnvironmentContext): Promise { + await this.initialize(); + return resolvePipenvPath(context.fsPath, this.nativeFinder, this.api, this); + } + + async clearCache?(): Promise { + await clearPipenvCache(); + this.collection = []; + this.fsPathToEnv.clear(); + this.globalEnv = undefined; + this._initialized = undefined; + } +} diff --git a/src/managers/pipenv/pipenvUtils.ts b/src/managers/pipenv/pipenvUtils.ts new file mode 100644 index 0000000..e490c73 --- /dev/null +++ b/src/managers/pipenv/pipenvUtils.ts @@ -0,0 +1,259 @@ +// Utility functions for Pipenv environment management + +import * as path from 'path'; +import { Uri } from 'vscode'; +import which from 'which'; +import { + EnvironmentManager, + PythonCommandRunConfiguration, + PythonEnvironment, + PythonEnvironmentApi, + PythonEnvironmentInfo, +} from '../../api'; +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { traceError, traceInfo } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { getSettingWorkspaceScope } from '../../features/settings/settingHelpers'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { getShellActivationCommands, shortVersion } from '../common/utils'; + +export const PIPENV_PATH_KEY = `${ENVS_EXTENSION_ID}:pipenv:PIPENV_PATH`; +export const PIPENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:pipenv:WORKSPACE_SELECTED`; +export const PIPENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:pipenv:GLOBAL_SELECTED`; + +let pipenvPath: string | undefined; + +async function findPipenv(): Promise { + try { + return await which('pipenv'); + } catch { + return undefined; + } +} + +async function setPipenv(pipenv: string): Promise { + pipenvPath = pipenv; + const state = await getWorkspacePersistentState(); + await state.set(PIPENV_PATH_KEY, pipenv); +} + +export async function clearPipenvCache(): Promise { + pipenvPath = undefined; +} + +function getPipenvPathFromSettings(): string | undefined { + const pipenvPath = getSettingWorkspaceScope('python', 'pipenvPath'); + return pipenvPath ? pipenvPath : undefined; +} + +export async function getPipenv(native?: NativePythonFinder): Promise { + if (pipenvPath) { + return pipenvPath; + } + + const state = await getWorkspacePersistentState(); + pipenvPath = await state.get(PIPENV_PATH_KEY); + if (pipenvPath) { + traceInfo(`Using pipenv from persistent state: ${pipenvPath}`); + return pipenvPath; + } + + // try to get from settings + const settingPath = getPipenvPathFromSettings(); + if (settingPath) { + pipenvPath = settingPath; + traceInfo(`Using pipenv from settings: ${settingPath}`); + return pipenvPath; + } + + // Try to find pipenv in PATH + const foundPipenv = await findPipenv(); + if (foundPipenv) { + pipenvPath = foundPipenv; + traceInfo(`Found pipenv in PATH: ${foundPipenv}`); + return foundPipenv; + } + + // Use native finder as fallback + if (native) { + const data = await native.refresh(false); + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pipenv'); + if (managers.length > 0) { + pipenvPath = managers[0].executable; + traceInfo(`Using pipenv from native finder: ${pipenvPath}`); + await state.set(PIPENV_PATH_KEY, pipenvPath); + return pipenvPath; + } + } + + traceInfo('Pipenv not found'); + return undefined; +} + +async function nativeToPythonEnv( + info: NativeEnvInfo, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + if (!(info.prefix && info.executable && info.version)) { + traceError(`Incomplete pipenv environment info: ${JSON.stringify(info)}`); + return undefined; + } + + const sv = shortVersion(info.version); + const folderName = path.basename(info.prefix); + const name = info.name || info.displayName || folderName; + const displayName = info.displayName || `${folderName} (${sv})`; + + // Derive the environment's bin/scripts directory from the python executable + const binDir = path.dirname(info.executable); + let shellActivation: Map = new Map(); + let shellDeactivation: Map = new Map(); + + try { + const maps = await getShellActivationCommands(binDir); + shellActivation = maps.shellActivation; + shellDeactivation = maps.shellDeactivation; + } catch (ex) { + traceError(`Failed to compute shell activation commands for pipenv at ${binDir}: ${ex}`); + } + + const environment: PythonEnvironmentInfo = { + name: name, + displayName: displayName, + shortDisplayName: displayName, + displayPath: info.prefix, + version: info.version, + environmentPath: Uri.file(info.prefix), + description: undefined, + tooltip: info.prefix, + execInfo: { + run: { executable: info.executable }, + shellActivation, + shellDeactivation, + }, + sysPrefix: info.prefix, + }; + + return api.createPythonEnvironmentItem(environment, manager); +} + +export async function refreshPipenv( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + traceInfo('Refreshing pipenv environments'); + + const searchPath = getPipenvPathFromSettings(); + const data = await nativeFinder.refresh(hardRefresh, searchPath ? [Uri.file(searchPath)] : undefined); + + let pipenv = await getPipenv(); + + if (pipenv === undefined) { + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pipenv'); + + if (managers.length > 0) { + pipenv = managers[0].executable; + await setPipenv(pipenv); + } + } + + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter((e) => e.kind === NativePythonEnvironmentKind.pipenv); + + const collection: PythonEnvironment[] = []; + + for (const e of envs) { + if (pipenv) { + const environment = await nativeToPythonEnv(e, api, manager); + if (environment) { + collection.push(environment); + } + } + } + + traceInfo(`Found ${collection.length} pipenv environments`); + return collection; +} + +export async function resolvePipenvPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + const resolved = await nativeFinder.resolve(fsPath); + + if (resolved.kind === NativePythonEnvironmentKind.pipenv) { + const pipenv = await getPipenv(nativeFinder); + if (pipenv) { + return await nativeToPythonEnv(resolved, api, manager); + } + } + + return undefined; +} + +// Persistence functions for workspace/global environment selection +export async function getPipenvForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + return await state.get(PIPENV_GLOBAL_KEY); +} + +export async function setPipenvForGlobal(pipenvPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(PIPENV_GLOBAL_KEY, pipenvPath); +} + +export async function getPipenvForWorkspace(fsPath: string): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(PIPENV_WORKSPACE_KEY); + if (data) { + try { + return data[fsPath]; + } catch { + return undefined; + } + } + return undefined; +} + +export async function setPipenvForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PIPENV_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(PIPENV_WORKSPACE_KEY, data); +} + +export async function setPipenvForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PIPENV_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(PIPENV_WORKSPACE_KEY, data); +} diff --git a/src/managers/poetry/main.ts b/src/managers/poetry/main.ts new file mode 100644 index 0000000..5f74fb4 --- /dev/null +++ b/src/managers/poetry/main.ts @@ -0,0 +1,45 @@ +import { Disposable, LogOutputChannel } from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { traceInfo } from '../../common/logging'; +import { getPythonApi } from '../../features/pythonApi'; +import { PythonProjectManager } from '../../internal.api'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { notifyMissingManagerIfDefault } from '../common/utils'; +import { PoetryManager } from './poetryManager'; +import { PoetryPackageManager } from './poetryPackageManager'; +import { getPoetry, getPoetryVersion } from './poetryUtils'; + +export async function registerPoetryFeatures( + nativeFinder: NativePythonFinder, + disposables: Disposable[], + outputChannel: LogOutputChannel, + projectManager: PythonProjectManager, +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + + try { + const poetryPath = await getPoetry(nativeFinder); + if (poetryPath) { + traceInfo( + 'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source `. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation.', + ); + const version = await getPoetryVersion(poetryPath); + traceInfo(`Poetry found at ${poetryPath}, version: ${version}`); + const envManager = new PoetryManager(nativeFinder, api); + const pkgManager = new PoetryPackageManager(api, outputChannel, envManager); + + disposables.push( + envManager, + pkgManager, + api.registerEnvironmentManager(envManager), + api.registerPackageManager(pkgManager), + ); + } else { + traceInfo('Poetry not found, turning off poetry features.'); + await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); + } + } catch (ex) { + traceInfo('Poetry not found, turning off poetry features.', ex); + await notifyMissingManagerIfDefault('ms-python.python:poetry', projectManager, api); + } +} diff --git a/src/managers/poetry/poetryManager.ts b/src/managers/poetry/poetryManager.ts new file mode 100644 index 0000000..12652ec --- /dev/null +++ b/src/managers/poetry/poetryManager.ts @@ -0,0 +1,332 @@ +import * as path from 'path'; +import { Disposable, EventEmitter, MarkdownString, ProgressLocation, Uri } from 'vscode'; +import { + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + PythonEnvironmentApi, + PythonProject, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../../api'; +import { PoetryStrings } from '../../common/localize'; +import { traceError, traceInfo } from '../../common/logging'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { withProgress } from '../../common/window.apis'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { getLatest } from '../common/utils'; +import { + clearPoetryCache, + getPoetryForGlobal, + getPoetryForWorkspace, + POETRY_GLOBAL, + refreshPoetry, + resolvePoetryPath, + setPoetryForGlobal, + setPoetryForWorkspace, + setPoetryForWorkspaces, +} from './poetryUtils'; + +export class PoetryManager implements EnvironmentManager, Disposable { + private collection: PythonEnvironment[] = []; + private fsPathToEnv: Map = new Map(); + private globalEnv: PythonEnvironment | undefined; + + private readonly _onDidChangeEnvironment = new EventEmitter(); + public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; + + private readonly _onDidChangeEnvironments = new EventEmitter(); + public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + + constructor(private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi) { + this.name = 'poetry'; + this.displayName = 'Poetry'; + this.preferredPackageManagerId = 'ms-python.python:poetry'; + this.tooltip = new MarkdownString(PoetryStrings.poetryManager, true); + } + + name: string; + displayName: string; + preferredPackageManagerId: string; + description?: string; + tooltip: string | MarkdownString; + iconPath?: IconPath; + + public dispose() { + this.collection = []; + this.fsPathToEnv.clear(); + } + + private _initialized: Deferred | undefined; + async initialize(): Promise { + if (this._initialized) { + return this._initialized.promise; + } + + this._initialized = createDeferred(); + + await withProgress( + { + location: ProgressLocation.Window, + title: PoetryStrings.poetryDiscovering, + }, + async () => { + this.collection = await refreshPoetry(false, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + this._onDidChangeEnvironments.fire( + this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })), + ); + }, + ); + this._initialized.resolve(); + } + + async getEnvironments(scope: GetEnvironmentsScope): Promise { + await this.initialize(); + + if (scope === 'all') { + return Array.from(this.collection); + } + + if (scope === 'global') { + return this.collection.filter((env) => { + return env.group === POETRY_GLOBAL; + }); + } + + if (scope instanceof Uri) { + const env = this.fromEnvMap(scope); + if (env) { + return [env]; + } + } + + return []; + } + + async refresh(context: RefreshEnvironmentsScope): Promise { + if (context === undefined) { + await withProgress( + { + location: ProgressLocation.Window, + title: PoetryStrings.poetryRefreshing, + }, + async () => { + traceInfo('Refreshing Poetry Environments'); + const discard = this.collection.map((c) => c); + this.collection = await refreshPoetry(true, this.nativeFinder, this.api, this); + + await this.loadEnvMap(); + + const args = [ + ...discard.map((env) => ({ kind: EnvironmentChangeKind.remove, environment: env })), + ...this.collection.map((env) => ({ kind: EnvironmentChangeKind.add, environment: env })), + ]; + + this._onDidChangeEnvironments.fire(args); + }, + ); + } + } + + async get(scope: GetEnvironmentScope): Promise { + await this.initialize(); + if (scope instanceof Uri) { + let env = this.fsPathToEnv.get(scope.fsPath); + if (env) { + return env; + } + const project = this.api.getPythonProject(scope); + if (project) { + env = this.fsPathToEnv.get(project.uri.fsPath); + if (env) { + return env; + } + } + } + + return this.globalEnv; + } + + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment | undefined): Promise { + if (scope === undefined) { + await setPoetryForGlobal(environment?.environmentPath?.fsPath); + } else if (scope instanceof Uri) { + const folder = this.api.getPythonProject(scope); + const fsPath = folder?.uri?.fsPath ?? scope.fsPath; + if (fsPath) { + if (environment) { + this.fsPathToEnv.set(fsPath, environment); + } else { + this.fsPathToEnv.delete(fsPath); + } + await setPoetryForWorkspace(fsPath, environment?.environmentPath?.fsPath); + } + } else if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setPoetryForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath?.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); + } + } + + async resolve(context: ResolveEnvironmentContext): Promise { + await this.initialize(); + + if (context instanceof Uri) { + const env = await resolvePoetryPath(context.fsPath, this.nativeFinder, this.api, this); + if (env) { + const _collectionEnv = this.findEnvironmentByPath(env.environmentPath.fsPath); + if (_collectionEnv) { + return _collectionEnv; + } + + this.collection.push(env); + this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: env }]); + + return env; + } + + return undefined; + } + } + + async clearCache(): Promise { + await clearPoetryCache(); + } + + private async loadEnvMap() { + this.globalEnv = undefined; + this.fsPathToEnv.clear(); + + // Try to find a global environment + const fsPath = await getPoetryForGlobal(); + + if (fsPath) { + this.globalEnv = this.findEnvironmentByPath(fsPath); + + // If the environment is not found, resolve the fsPath + if (!this.globalEnv) { + this.globalEnv = await resolvePoetryPath(fsPath, this.nativeFinder, this.api, this); + + // If the environment is resolved, add it to the collection + if (this.globalEnv) { + this.collection.push(this.globalEnv); + } + } + } + + if (!this.globalEnv) { + this.globalEnv = getLatest(this.collection.filter((e) => e.group === POETRY_GLOBAL)); + } + + // Find any poetry environments that might be associated with the current projects + // Poetry typically has a pyproject.toml file in the project root + const pathSorted = this.collection + .filter((e) => this.api.getPythonProject(e.environmentPath)) + .sort((a, b) => { + if (a.environmentPath.fsPath !== b.environmentPath.fsPath) { + return a.environmentPath.fsPath.length - b.environmentPath.fsPath.length; + } + return a.environmentPath.fsPath.localeCompare(b.environmentPath.fsPath); + }); + + // Try to find workspace environments + const paths = this.api.getPythonProjects().map((p) => p.uri.fsPath); + for (const p of paths) { + const env = await getPoetryForWorkspace(p); + + if (env) { + const found = this.findEnvironmentByPath(env); + + if (found) { + this.fsPathToEnv.set(p, found); + } else { + // If not found, resolve the poetry path + const resolved = await resolvePoetryPath(env, this.nativeFinder, this.api, this); + + if (resolved) { + // If resolved add it to the collection + this.fsPathToEnv.set(p, resolved); + this.collection.push(resolved); + } else { + traceError(`Failed to resolve poetry environment: ${env}`); + } + } + } else { + // If there is not an environment already assigned by user to this project + // then see if there is one in the collection + if (pathSorted.length === 1) { + this.fsPathToEnv.set(p, pathSorted[0]); + } else { + // If there is more than one environment then we need to check if the project + // is a subfolder of one of the environments + const found = pathSorted.find((e) => { + const t = this.api.getPythonProject(e.environmentPath)?.uri.fsPath; + return t && path.normalize(t) === p; + }); + if (found) { + this.fsPathToEnv.set(p, found); + } + } + } + } + } + + private fromEnvMap(uri: Uri): PythonEnvironment | undefined { + // Find environment directly using the URI mapping + const env = this.fsPathToEnv.get(uri.fsPath); + if (env) { + return env; + } + + // Find environment using the Python project for the Uri + const project = this.api.getPythonProject(uri); + if (project) { + return this.fsPathToEnv.get(project.uri.fsPath); + } + + return undefined; + } + + private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { + const normalized = path.normalize(fsPath); + return this.collection.find((e) => { + const n = path.normalize(e.environmentPath.fsPath); + return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized; + }); + } +} diff --git a/src/managers/poetry/poetryPackageManager.ts b/src/managers/poetry/poetryPackageManager.ts new file mode 100644 index 0000000..d3379e9 --- /dev/null +++ b/src/managers/poetry/poetryPackageManager.ts @@ -0,0 +1,303 @@ +import * as ch from 'child_process'; +import * as fsapi from 'fs-extra'; +import * as path from 'path'; +import { + CancellationError, + CancellationToken, + Event, + EventEmitter, + LogOutputChannel, + MarkdownString, + ProgressLocation, + ThemeIcon, +} from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { + DidChangePackagesEventArgs, + IconPath, + Package, + PackageChangeKind, + PackageManagementOptions, + PackageManager, + PythonEnvironment, + PythonEnvironmentApi, +} from '../../api'; +import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis'; +import { PoetryManager } from './poetryManager'; +import { getPoetry } from './poetryUtils'; + +function getChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] { + const changes: { kind: PackageChangeKind; pkg: Package }[] = []; + before.forEach((pkg) => { + changes.push({ kind: PackageChangeKind.remove, pkg }); + }); + after.forEach((pkg) => { + changes.push({ kind: PackageChangeKind.add, pkg }); + }); + return changes; +} + +export class PoetryPackageManager implements PackageManager, Disposable { + private readonly _onDidChangePackages = new EventEmitter(); + onDidChangePackages: Event = this._onDidChangePackages.event; + + private packages: Map = new Map(); + + constructor( + private readonly api: PythonEnvironmentApi, + public readonly log: LogOutputChannel, + _poetry: PoetryManager, + ) { + this.name = 'poetry'; + this.displayName = 'Poetry'; + this.description = 'This package manager for Python uses Poetry for package management.'; + this.tooltip = new MarkdownString('This package manager for Python uses `poetry` for package management.'); + this.iconPath = new ThemeIcon('package'); + } + readonly name: string; + readonly displayName?: string; + readonly description?: string; + readonly tooltip?: string | MarkdownString; + readonly iconPath?: IconPath; + + async manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise { + let toInstall: string[] = [...(options.install ?? [])]; + let toUninstall: string[] = [...(options.uninstall ?? [])]; + + if (toInstall.length === 0 && toUninstall.length === 0) { + // Show package input UI if no packages are specified + const installInput = await showInputBox({ + prompt: 'Enter packages to install (comma separated)', + placeHolder: 'e.g., requests, pytest, black', + }); + + if (installInput) { + toInstall = installInput + .split(',') + .map((p) => p.trim()) + .filter((p) => p.length > 0); + } + + if (toInstall.length === 0) { + return; + } + } + + await withProgress( + { + location: ProgressLocation.Notification, + title: 'Managing packages with Poetry', + cancellable: true, + }, + async (_progress, token) => { + try { + const before = this.packages.get(environment.envId.id) ?? []; + const after = await this.managePackages( + environment, + { install: toInstall, uninstall: toUninstall }, + token, + ); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } catch (e) { + if (e instanceof CancellationError) { + throw e; + } + this.log.error('Error managing packages with Poetry', e); + setImmediate(async () => { + const result = await showErrorMessage('Error managing packages with Poetry', 'View Output'); + if (result === 'View Output') { + this.log.show(); + } + }); + throw e; + } + }, + ); + } + + async refresh(environment: PythonEnvironment): Promise { + await withProgress( + { + location: ProgressLocation.Window, + title: 'Refreshing Poetry packages', + }, + async () => { + try { + const before = this.packages.get(environment.envId.id) ?? []; + const after = await this.refreshPackages(environment); + const changes = getChanges(before, after); + this.packages.set(environment.envId.id, after); + if (changes.length > 0) { + this._onDidChangePackages.fire({ environment, manager: this, changes }); + } + } catch (error) { + this.log.error(`Failed to refresh packages: ${error}`); + // Show error to user but don't break the UI + setImmediate(async () => { + const result = await showErrorMessage('Error refreshing Poetry packages', 'View Output'); + if (result === 'View Output') { + this.log.show(); + } + }); + } + }, + ); + } + + async getPackages(environment: PythonEnvironment): Promise { + if (!this.packages.has(environment.envId.id)) { + await this.refresh(environment); + } + return this.packages.get(environment.envId.id); + } + + dispose(): void { + this._onDidChangePackages.dispose(); + this.packages.clear(); + } + + private async managePackages( + environment: PythonEnvironment, + options: { install?: string[]; uninstall?: string[] }, + token?: CancellationToken, + ): Promise { + // Handle uninstalls first + if (options.uninstall && options.uninstall.length > 0) { + try { + const args = ['remove', ...options.uninstall]; + this.log.info(`Running: poetry ${args.join(' ')}`); + const result = await runPoetry(args, undefined, this.log, token); + this.log.info(result); + } catch (err) { + this.log.error(`Error removing packages with Poetry: ${err}`); + throw err; + } + } + + // Handle installs + if (options.install && options.install.length > 0) { + try { + const args = ['add', ...options.install]; + this.log.info(`Running: poetry ${args.join(' ')}`); + const result = await runPoetry(args, undefined, this.log, token); + this.log.info(result); + } catch (err) { + this.log.error(`Error adding packages with Poetry: ${err}`); + throw err; + } + } + + // Refresh the packages list after changes + return this.refreshPackages(environment); + } + + private async refreshPackages(environment: PythonEnvironment): Promise { + let cwd = process.cwd(); + const projects = this.api.getPythonProjects(); + if (projects.length === 1) { + const stat = await fsapi.stat(projects[0].uri.fsPath); + if (stat.isDirectory()) { + cwd = projects[0].uri.fsPath; + } else { + cwd = path.dirname(projects[0].uri.fsPath); + } + } else if (projects.length > 1) { + const dirs = new Set(); + await Promise.all( + projects.map(async (project) => { + const e = await this.api.getEnvironment(project.uri); + if (e?.envId.id === environment.envId.id) { + const stat = await fsapi.stat(projects[0].uri.fsPath); + const dir = stat.isDirectory() ? projects[0].uri.fsPath : path.dirname(projects[0].uri.fsPath); + if (dirs.has(dir)) { + dirs.add(dir); + } + } + }), + ); + if (dirs.size > 0) { + // ensure we have the deepest directory node picked + cwd = Array.from(dirs.values()).sort((a, b) => (a.length - b.length) * -1)[0]; + } + } + + const poetryPackages: { name: string; version: string; displayName: string; description: string }[] = []; + + try { + this.log.info(`Running: ${await getPoetry()} show --no-ansi`); + const result = await runPoetry(['show', '--no-ansi'], cwd, this.log); + + // Parse poetry show output + // Format: name version description + const lines = result.split('\n'); + for (const line of lines) { + // Updated regex to properly handle lines with the format: + // "package (!) version description" + const match = line.match(/^(\S+)(?:\s+\([!]\))?\s+(\S+)\s+(.*)/); + if (match) { + const [, name, version, description] = match; + poetryPackages.push({ + name, + version, + displayName: name, + description: `${version} - ${description?.trim() || ''}`, + }); + } + } + } catch (err) { + this.log.error(`Error refreshing packages with Poetry: ${err}`); + // Return empty array instead of throwing to avoid breaking the UI + return []; + } + + // Convert to Package objects using the API + return poetryPackages.map((pkg) => this.api.createPackageItem(pkg, environment, this)); + } +} + +export async function runPoetry( + args: string[], + cwd?: string, + log?: LogOutputChannel, + token?: CancellationToken, +): Promise { + const poetry = await getPoetry(); + if (!poetry) { + throw new Error('Poetry executable not found'); + } + + log?.info(`Running: ${poetry} ${args.join(' ')}`); + + return new Promise((resolve, reject) => { + const proc = ch.spawn(poetry, args, { cwd }); + token?.onCancellationRequested(() => { + proc.kill(); + reject(new CancellationError()); + }); + let builder = ''; + proc.stdout?.on('data', (data) => { + const s = data.toString('utf-8'); + builder += s; + log?.append(`poetry: ${s}`); + }); + proc.stderr?.on('data', (data) => { + const s = data.toString('utf-8'); + builder += s; + log?.append(`poetry: ${s}`); + }); + proc.on('close', () => { + resolve(builder); + }); + proc.on('error', (error) => { + log?.error(`Error executing poetry command: ${error}`); + reject(error); + }); + proc.on('exit', (code) => { + if (code !== 0) { + reject(new Error(`Failed to run poetry ${args.join(' ')}`)); + } + }); + }); +} diff --git a/src/managers/poetry/poetryUtils.ts b/src/managers/poetry/poetryUtils.ts new file mode 100644 index 0000000..50b1ecc --- /dev/null +++ b/src/managers/poetry/poetryUtils.ts @@ -0,0 +1,374 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import which from 'which'; +import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api'; +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { traceError, traceInfo } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { getUserHomeDir, untildify } from '../../common/utils/pathUtils'; +import { isWindows } from '../../common/utils/platformUtils'; +import { getConfiguration } from '../../common/workspace.apis'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils'; + +async function findPoetry(): Promise { + try { + return await which('poetry'); + } catch { + return undefined; + } +} + +export const POETRY_GLOBAL = 'Global'; + +export const POETRY_PATH_KEY = `${ENVS_EXTENSION_ID}:poetry:POETRY_PATH`; +export const POETRY_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:poetry:WORKSPACE_SELECTED`; +export const POETRY_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:poetry:GLOBAL_SELECTED`; +export const POETRY_VIRTUALENVS_PATH_KEY = `${ENVS_EXTENSION_ID}:poetry:VIRTUALENVS_PATH`; + +let poetryPath: string | undefined; +let poetryVirtualenvsPath: string | undefined; + +export async function clearPoetryCache(): Promise { + // Reset in-memory cache + poetryPath = undefined; + poetryVirtualenvsPath = undefined; +} + +function getPoetryPathFromSettings(): string | undefined { + const config = getConfiguration('python'); + const value = config.get('poetryPath'); + return value && typeof value === 'string' ? untildify(value) : value; +} + +async function setPoetry(poetry: string): Promise { + poetryPath = poetry; + const state = await getWorkspacePersistentState(); + await state.set(POETRY_PATH_KEY, poetry); + + // Also get and cache the virtualenvs path + await getPoetryVirtualenvsPath(poetry); +} + +export async function getPoetryForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + return await state.get(POETRY_GLOBAL_KEY); +} + +export async function setPoetryForGlobal(poetryPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(POETRY_GLOBAL_KEY, poetryPath); +} + +export async function getPoetryForWorkspace(fsPath: string): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(POETRY_WORKSPACE_KEY); + if (data) { + try { + return data[fsPath]; + } catch { + return undefined; + } + } + return undefined; +} + +export async function setPoetryForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(POETRY_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(POETRY_WORKSPACE_KEY, data); +} + +export async function setPoetryForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(POETRY_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(POETRY_WORKSPACE_KEY, data); +} + +export async function getPoetry(native?: NativePythonFinder): Promise { + if (poetryPath) { + return poetryPath; + } + + const state = await getWorkspacePersistentState(); + poetryPath = await state.get(POETRY_PATH_KEY); + if (poetryPath) { + traceInfo(`Using poetry from persistent state: ${poetryPath}`); + // Also retrieve the virtualenvs path if we haven't already + if (!poetryVirtualenvsPath) { + getPoetryVirtualenvsPath(untildify(poetryPath)).catch((e) => + traceError(`Error getting Poetry virtualenvs path: ${e}`), + ); + } + return untildify(poetryPath); + } + + // try to get from settings + const settingPath = getPoetryPathFromSettings(); + if (settingPath) { + poetryPath = settingPath; + traceInfo(`Using poetry from settings: ${settingPath}`); + return poetryPath; + } + + // Check in standard PATH locations + poetryPath = await findPoetry(); + if (poetryPath) { + await setPoetry(poetryPath); + return poetryPath; + } + + // Check for user-installed poetry + const home = getUserHomeDir(); + if (home) { + const poetryUserInstall = path.join( + home, + isWindows() ? 'AppData\\Roaming\\Python\\Scripts\\poetry.exe' : '.local/bin/poetry', + ); + if (await fs.exists(poetryUserInstall)) { + poetryPath = poetryUserInstall; + await setPoetry(poetryPath); + return poetryPath; + } + } + + if (native) { + const data = await native.refresh(false); + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'poetry'); + if (managers.length > 0) { + poetryPath = managers[0].executable; + traceInfo(`Using poetry from native finder: ${poetryPath}`); + await setPoetry(poetryPath); + return poetryPath; + } + } + + return undefined; +} + +export async function getPoetryVirtualenvsPath(poetryExe?: string): Promise { + if (poetryVirtualenvsPath) { + return poetryVirtualenvsPath; + } + + // Check if we have it in persistent state + const state = await getWorkspacePersistentState(); + poetryVirtualenvsPath = await state.get(POETRY_VIRTUALENVS_PATH_KEY); + if (poetryVirtualenvsPath) { + return untildify(poetryVirtualenvsPath); + } + + // Try to get it from poetry config + const poetry = poetryExe || (await getPoetry()); + if (poetry) { + try { + const { stdout } = await exec(`"${poetry}" config virtualenvs.path`); + if (stdout) { + const venvPath = stdout.trim(); + // Poetry might return the path with placeholders like {cache-dir} + // If it doesn't start with / or C:\ etc., assume it's using default + if (!path.isAbsolute(venvPath) || venvPath.includes('{')) { + const home = getUserHomeDir(); + if (home) { + poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs'); + } + } else { + poetryVirtualenvsPath = venvPath; + } + + if (poetryVirtualenvsPath) { + await state.set(POETRY_VIRTUALENVS_PATH_KEY, poetryVirtualenvsPath); + return poetryVirtualenvsPath; + } + } + } catch (e) { + traceError(`Error getting Poetry virtualenvs path: ${e}`); + } + } + + // Fallback to default location + const home = getUserHomeDir(); + if (home) { + poetryVirtualenvsPath = path.join(home, '.cache', 'pypoetry', 'virtualenvs'); + await state.set(POETRY_VIRTUALENVS_PATH_KEY, poetryVirtualenvsPath); + return poetryVirtualenvsPath; + } + + return undefined; +} + +// These are now exported for use in main.ts or environment manager logic +import * as cp from 'child_process'; +import { promisify } from 'util'; + +const exec = promisify(cp.exec); + +export async function getPoetryVersion(poetry: string): Promise { + try { + const { stdout } = await exec(`"${poetry}" --version`); + // Handle both formats: + // Old: "Poetry version 1.5.1" + // New: "Poetry (version 2.1.3)" + traceInfo(`Poetry version output: ${stdout.trim()}`); + const match = stdout.match(/Poetry (?:version|[\(\s]+version[\s\)]+)([0-9]+\.[0-9]+\.[0-9]+)/i); + return match ? match[1] : undefined; + } catch { + return undefined; + } +} +async function nativeToPythonEnv( + info: NativeEnvInfo, + api: PythonEnvironmentApi, + manager: EnvironmentManager, + _poetry: string, +): Promise { + if (!(info.prefix && info.executable && info.version)) { + traceError(`Incomplete poetry environment info: ${JSON.stringify(info)}`); + return undefined; + } + + const sv = shortVersion(info.version); + const name = info.name || info.displayName || path.basename(info.prefix); + const displayName = info.displayName || `poetry (${sv})`; + + // Check if this is a global Poetry virtualenv by checking if it's in Poetry's virtualenvs directory + // We need to use path.normalize() to ensure consistent path format comparison + const normalizedPrefix = path.normalize(info.prefix); + + // Determine if the environment is in Poetry's global virtualenvs directory + let isGlobalPoetryEnv = false; + const virtualenvsPath = poetryVirtualenvsPath; // Use the cached value if available + if (virtualenvsPath) { + const normalizedVirtualenvsPath = path.normalize(virtualenvsPath); + isGlobalPoetryEnv = normalizedPrefix.startsWith(normalizedVirtualenvsPath); + } else { + // Fall back to checking the default location if we haven't cached the path yet + const homeDir = getUserHomeDir(); + if (homeDir) { + const defaultPath = path.normalize(path.join(homeDir, '.cache', 'pypoetry', 'virtualenvs')); + isGlobalPoetryEnv = normalizedPrefix.startsWith(defaultPath); + + // Try to get the actual path asynchronously for next time + getPoetryVirtualenvsPath(_poetry).catch((e) => traceError(`Error getting Poetry virtualenvs path: ${e}`)); + } + } + + // Get generic python environment info to access shell activation/deactivation commands following Poetry 2.0+ dropping the `shell` command + const binDir = path.dirname(info.executable); + const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir); + + const environment: PythonEnvironmentInfo = { + name: name, + displayName: displayName, + shortDisplayName: displayName, + displayPath: info.prefix, + version: info.version, + environmentPath: Uri.file(info.prefix), + description: undefined, + tooltip: info.prefix, + execInfo: { + run: { executable: info.executable }, + shellActivation, + shellDeactivation, + }, + sysPrefix: info.prefix, + group: isGlobalPoetryEnv ? POETRY_GLOBAL : undefined, + }; + + return api.createPythonEnvironmentItem(environment, manager); +} + +export async function refreshPoetry( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + traceInfo('Refreshing poetry environments'); + + const searchPath = getPoetryPathFromSettings(); + const data = await nativeFinder.refresh(hardRefresh, searchPath ? [Uri.file(searchPath)] : undefined); + + let poetry = await getPoetry(); + + if (poetry === undefined) { + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'poetry'); + if (managers.length > 0) { + poetry = managers[0].executable; + await setPoetry(poetry); + } + } + + if (!poetry) { + traceInfo('Poetry executable not found'); + return []; + } + + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter((e) => e.kind === NativePythonEnvironmentKind.poetry); + + const collection: PythonEnvironment[] = []; + + await Promise.all( + envs.map(async (e) => { + if (poetry) { + const environment = await nativeToPythonEnv(e, api, manager, poetry); + if (environment) { + collection.push(environment); + } + } + }), + ); + + return sortEnvironments(collection); +} + +export async function resolvePoetryPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + try { + const e = await nativeFinder.resolve(fsPath); + if (e.kind !== NativePythonEnvironmentKind.poetry) { + return undefined; + } + const poetry = await getPoetry(nativeFinder); + if (!poetry) { + traceError('Poetry not found while resolving environment'); + return undefined; + } + + return nativeToPythonEnv(e, api, manager, poetry); + } catch { + return undefined; + } +} diff --git a/src/managers/pyenv/main.ts b/src/managers/pyenv/main.ts new file mode 100644 index 0000000..2ca6a96 --- /dev/null +++ b/src/managers/pyenv/main.ts @@ -0,0 +1,32 @@ +import { Disposable } from 'vscode'; +import { PythonEnvironmentApi } from '../../api'; +import { traceInfo } from '../../common/logging'; +import { getPythonApi } from '../../features/pythonApi'; +import { PythonProjectManager } from '../../internal.api'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { notifyMissingManagerIfDefault } from '../common/utils'; +import { PyEnvManager } from './pyenvManager'; +import { getPyenv } from './pyenvUtils'; + +export async function registerPyenvFeatures( + nativeFinder: NativePythonFinder, + disposables: Disposable[], + projectManager: PythonProjectManager, +): Promise { + const api: PythonEnvironmentApi = await getPythonApi(); + + try { + const pyenv = await getPyenv(nativeFinder); + + if (pyenv) { + const mgr = new PyEnvManager(nativeFinder, api); + disposables.push(mgr, api.registerEnvironmentManager(mgr)); + } else { + traceInfo('Pyenv not found, turning off pyenv features.'); + await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); + } + } catch (ex) { + traceInfo('Pyenv not found, turning off pyenv features.', ex); + await notifyMissingManagerIfDefault('ms-python.python:pyenv', projectManager, api); + } +} diff --git a/src/managers/pyenv/pyenvManager.ts b/src/managers/pyenv/pyenvManager.ts new file mode 100644 index 0000000..a70c1b6 --- /dev/null +++ b/src/managers/pyenv/pyenvManager.ts @@ -0,0 +1,331 @@ +import * as path from 'path'; +import { Disposable, EventEmitter, MarkdownString, ProgressLocation, Uri } from 'vscode'; +import { + DidChangeEnvironmentEventArgs, + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + EnvironmentManager, + GetEnvironmentScope, + GetEnvironmentsScope, + IconPath, + PythonEnvironment, + PythonEnvironmentApi, + PythonProject, + RefreshEnvironmentsScope, + ResolveEnvironmentContext, + SetEnvironmentScope, +} from '../../api'; +import { PyenvStrings } from '../../common/localize'; +import { traceError, traceInfo } from '../../common/logging'; +import { createDeferred, Deferred } from '../../common/utils/deferred'; +import { withProgress } from '../../common/window.apis'; +import { NativePythonFinder } from '../common/nativePythonFinder'; +import { getLatest } from '../common/utils'; +import { + clearPyenvCache, + getPyenvForGlobal, + getPyenvForWorkspace, + PYENV_VERSIONS, + refreshPyenv, + resolvePyenvPath, + setPyenvForGlobal, + setPyenvForWorkspace, + setPyenvForWorkspaces, +} from './pyenvUtils'; + +export class PyEnvManager implements EnvironmentManager, Disposable { + private collection: PythonEnvironment[] = []; + private fsPathToEnv: Map = new Map(); + private globalEnv: PythonEnvironment | undefined; + + private readonly _onDidChangeEnvironment = new EventEmitter(); + public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; + + private readonly _onDidChangeEnvironments = new EventEmitter(); + public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; + + constructor(private readonly nativeFinder: NativePythonFinder, private readonly api: PythonEnvironmentApi) { + this.name = 'pyenv'; + this.displayName = 'PyEnv'; + this.preferredPackageManagerId = 'ms-python.python:pip'; + this.tooltip = new MarkdownString(PyenvStrings.pyenvManager, true); + } + + name: string; + displayName: string; + preferredPackageManagerId: string; + description?: string; + tooltip: string | MarkdownString; + iconPath?: IconPath; + + public dispose() { + this.collection = []; + this.fsPathToEnv.clear(); + } + + private _initialized: Deferred | undefined; + async initialize(): Promise { + if (this._initialized) { + return this._initialized.promise; + } + + this._initialized = createDeferred(); + + await withProgress( + { + location: ProgressLocation.Window, + title: PyenvStrings.pyenvDiscovering, + }, + async () => { + this.collection = await refreshPyenv(false, this.nativeFinder, this.api, this); + await this.loadEnvMap(); + + this._onDidChangeEnvironments.fire( + this.collection.map((e) => ({ environment: e, kind: EnvironmentChangeKind.add })), + ); + }, + ); + this._initialized.resolve(); + } + + async getEnvironments(scope: GetEnvironmentsScope): Promise { + await this.initialize(); + + if (scope === 'all') { + return Array.from(this.collection); + } + + if (scope === 'global') { + return this.collection.filter((env) => { + env.group === PYENV_VERSIONS; + }); + } + + if (scope instanceof Uri) { + const env = this.fromEnvMap(scope); + if (env) { + return [env]; + } + } + + return []; + } + + async refresh(context: RefreshEnvironmentsScope): Promise { + if (context === undefined) { + await withProgress( + { + location: ProgressLocation.Window, + title: PyenvStrings.pyenvRefreshing, + }, + async () => { + traceInfo('Refreshing Pyenv Environments'); + const discard = this.collection.map((c) => c); + this.collection = await refreshPyenv(true, this.nativeFinder, this.api, this); + + await this.loadEnvMap(); + + const args = [ + ...discard.map((env) => ({ kind: EnvironmentChangeKind.remove, environment: env })), + ...this.collection.map((env) => ({ kind: EnvironmentChangeKind.add, environment: env })), + ]; + + this._onDidChangeEnvironments.fire(args); + }, + ); + } + } + + async get(scope: GetEnvironmentScope): Promise { + await this.initialize(); + if (scope instanceof Uri) { + let env = this.fsPathToEnv.get(scope.fsPath); + if (env) { + return env; + } + const project = this.api.getPythonProject(scope); + if (project) { + env = this.fsPathToEnv.get(project.uri.fsPath); + if (env) { + return env; + } + } + } + + return this.globalEnv; + } + async set(scope: SetEnvironmentScope, environment?: PythonEnvironment | undefined): Promise { + if (scope === undefined) { + await setPyenvForGlobal(environment?.environmentPath?.fsPath); + } else if (scope instanceof Uri) { + const folder = this.api.getPythonProject(scope); + const fsPath = folder?.uri?.fsPath ?? scope.fsPath; + if (fsPath) { + if (environment) { + this.fsPathToEnv.set(fsPath, environment); + } else { + this.fsPathToEnv.delete(fsPath); + } + await setPyenvForWorkspace(fsPath, environment?.environmentPath?.fsPath); + } + } else if (Array.isArray(scope) && scope.every((u) => u instanceof Uri)) { + const projects: PythonProject[] = []; + scope + .map((s) => this.api.getPythonProject(s)) + .forEach((p) => { + if (p) { + projects.push(p); + } + }); + + const before: Map = new Map(); + projects.forEach((p) => { + before.set(p.uri.fsPath, this.fsPathToEnv.get(p.uri.fsPath)); + if (environment) { + this.fsPathToEnv.set(p.uri.fsPath, environment); + } else { + this.fsPathToEnv.delete(p.uri.fsPath); + } + }); + + await setPyenvForWorkspaces( + projects.map((p) => p.uri.fsPath), + environment?.environmentPath?.fsPath, + ); + + projects.forEach((p) => { + const b = before.get(p.uri.fsPath); + if (b?.envId.id !== environment?.envId.id) { + this._onDidChangeEnvironment.fire({ uri: p.uri, old: b, new: environment }); + } + }); + } + } + + async resolve(context: ResolveEnvironmentContext): Promise { + await this.initialize(); + + if (context instanceof Uri) { + const env = await resolvePyenvPath(context.fsPath, this.nativeFinder, this.api, this); + if (env) { + const _collectionEnv = this.findEnvironmentByPath(env.environmentPath.fsPath); + if (_collectionEnv) { + return _collectionEnv; + } + + this.collection.push(env); + this._onDidChangeEnvironments.fire([{ kind: EnvironmentChangeKind.add, environment: env }]); + + return env; + } + + return undefined; + } + } + + async clearCache(): Promise { + await clearPyenvCache(); + } + + private async loadEnvMap() { + this.globalEnv = undefined; + this.fsPathToEnv.clear(); + + // Try to find a global environment + const fsPath = await getPyenvForGlobal(); + + if (fsPath) { + this.globalEnv = this.findEnvironmentByPath(fsPath); + + // If the environment is not found, resolve the fsPath. Could be portable conda. + if (!this.globalEnv) { + this.globalEnv = await resolvePyenvPath(fsPath, this.nativeFinder, this.api, this); + + // If the environment is resolved, add it to the collection + if (this.globalEnv) { + this.collection.push(this.globalEnv); + } + } + } + + if (!this.globalEnv) { + this.globalEnv = getLatest(this.collection.filter((e) => e.group === PYENV_VERSIONS)); + } + + // Find any pyenv environments that might be associated with the current projects + // These are environments whose parent dirs are project dirs. + const pathSorted = this.collection + .filter((e) => this.api.getPythonProject(e.environmentPath)) + .sort((a, b) => { + if (a.environmentPath.fsPath !== b.environmentPath.fsPath) { + return a.environmentPath.fsPath.length - b.environmentPath.fsPath.length; + } + return a.environmentPath.fsPath.localeCompare(b.environmentPath.fsPath); + }); + + // Try to find workspace environments + const paths = this.api.getPythonProjects().map((p) => p.uri.fsPath); + for (const p of paths) { + const env = await getPyenvForWorkspace(p); + + if (env) { + const found = this.findEnvironmentByPath(env); + + if (found) { + this.fsPathToEnv.set(p, found); + } else { + // If not found, resolve the pyenv path. Could be portable pyenv. + const resolved = await resolvePyenvPath(env, this.nativeFinder, this.api, this); + + if (resolved) { + // If resolved add it to the collection + this.fsPathToEnv.set(p, resolved); + this.collection.push(resolved); + } else { + traceError(`Failed to resolve pyenv environment: ${env}`); + } + } + } else { + // If there is not an environment already assigned by user to this project + // then see if there is one in the collection + if (pathSorted.length === 1) { + this.fsPathToEnv.set(p, pathSorted[0]); + } else { + // If there is more than one environment then we need to check if the project + // is a subfolder of one of the environments + const found = pathSorted.find((e) => { + const t = this.api.getPythonProject(e.environmentPath)?.uri.fsPath; + return t && path.normalize(t) === p; + }); + if (found) { + this.fsPathToEnv.set(p, found); + } + } + } + } + } + + private fromEnvMap(uri: Uri): PythonEnvironment | undefined { + // Find environment directly using the URI mapping + const env = this.fsPathToEnv.get(uri.fsPath); + if (env) { + return env; + } + + // Find environment using the Python project for the Uri + const project = this.api.getPythonProject(uri); + if (project) { + return this.fsPathToEnv.get(project.uri.fsPath); + } + + return undefined; + } + + private findEnvironmentByPath(fsPath: string): PythonEnvironment | undefined { + const normalized = path.normalize(fsPath); + return this.collection.find((e) => { + const n = path.normalize(e.environmentPath.fsPath); + return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized; + }); + } +} diff --git a/src/managers/pyenv/pyenvUtils.ts b/src/managers/pyenv/pyenvUtils.ts new file mode 100644 index 0000000..026d3f1 --- /dev/null +++ b/src/managers/pyenv/pyenvUtils.ts @@ -0,0 +1,278 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import which from 'which'; +import { + EnvironmentManager, + PythonCommandRunConfiguration, + PythonEnvironment, + PythonEnvironmentApi, + PythonEnvironmentInfo, +} from '../../api'; +import { ENVS_EXTENSION_ID } from '../../common/constants'; +import { traceError, traceInfo } from '../../common/logging'; +import { getWorkspacePersistentState } from '../../common/persistentState'; +import { getUserHomeDir, normalizePath, untildify } from '../../common/utils/pathUtils'; +import { isWindows } from '../../common/utils/platformUtils'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonEnvironmentKind, + NativePythonFinder, +} from '../common/nativePythonFinder'; +import { shortVersion, sortEnvironments } from '../common/utils'; + +async function findPyenv(): Promise { + try { + return await which('pyenv'); + } catch { + return undefined; + } +} + +export const PYENV_ENVIRONMENTS = 'Environments'; +export const PYENV_VERSIONS = 'Versions'; + +export const PYENV_PATH_KEY = `${ENVS_EXTENSION_ID}:pyenv:PYENV_PATH`; +export const PYENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:pyenv:WORKSPACE_SELECTED`; +export const PYENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:pyenv:GLOBAL_SELECTED`; + +let pyenvPath: string | undefined; +export async function clearPyenvCache(): Promise { + pyenvPath = undefined; +} + +async function setPyenv(pyenv: string): Promise { + pyenvPath = pyenv; + const state = await getWorkspacePersistentState(); + await state.set(PYENV_PATH_KEY, pyenv); +} + +export async function getPyenvForGlobal(): Promise { + const state = await getWorkspacePersistentState(); + return await state.get(PYENV_GLOBAL_KEY); +} + +export async function setPyenvForGlobal(pyenvPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + await state.set(PYENV_GLOBAL_KEY, pyenvPath); +} + +export async function getPyenvForWorkspace(fsPath: string): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } | undefined = await state.get(PYENV_WORKSPACE_KEY); + if (data) { + try { + return data[fsPath]; + } catch { + return undefined; + } + } + return undefined; +} + +export async function setPyenvForWorkspace(fsPath: string, envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PYENV_WORKSPACE_KEY)) ?? {}; + if (envPath) { + data[fsPath] = envPath; + } else { + delete data[fsPath]; + } + await state.set(PYENV_WORKSPACE_KEY, data); +} + +export async function setPyenvForWorkspaces(fsPath: string[], envPath: string | undefined): Promise { + const state = await getWorkspacePersistentState(); + const data: { [key: string]: string } = (await state.get(PYENV_WORKSPACE_KEY)) ?? {}; + fsPath.forEach((s) => { + if (envPath) { + data[s] = envPath; + } else { + delete data[s]; + } + }); + await state.set(PYENV_WORKSPACE_KEY, data); +} + +export async function getPyenv(native?: NativePythonFinder): Promise { + if (pyenvPath) { + return pyenvPath; + } + + const state = await getWorkspacePersistentState(); + pyenvPath = await state.get(PYENV_PATH_KEY); + if (pyenvPath) { + traceInfo(`Using pyenv from persistent state: ${pyenvPath}`); + return untildify(pyenvPath); + } + + const pyenvBin = isWindows() ? 'pyenv.exe' : 'pyenv'; + const pyenvRoot = process.env.PYENV_ROOT; + if (pyenvRoot) { + const pyenvPath = path.join(pyenvRoot, 'bin', pyenvBin); + if (await fs.exists(pyenvPath)) { + return pyenvPath; + } + } + + const home = getUserHomeDir(); + if (home) { + const pyenvPath = path.join(home, '.pyenv', 'bin', pyenvBin); + if (await fs.exists(pyenvPath)) { + return pyenvPath; + } + + if (isWindows()) { + const pyenvPathWin = path.join(home, '.pyenv', 'pyenv-win', 'bin', pyenvBin); + if (await fs.exists(pyenvPathWin)) { + return pyenvPathWin; + } + } + } + + pyenvPath = await findPyenv(); + if (pyenvPath) { + return pyenvPath; + } + + if (native) { + const data = await native.refresh(false); + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pyenv'); + if (managers.length > 0) { + pyenvPath = managers[0].executable; + traceInfo(`Using pyenv from native finder: ${pyenvPath}`); + await state.set(PYENV_PATH_KEY, pyenvPath); + return pyenvPath; + } + } + + return undefined; +} + +function nativeToPythonEnv( + info: NativeEnvInfo, + api: PythonEnvironmentApi, + manager: EnvironmentManager, + pyenv: string, +): PythonEnvironment | undefined { + if (!(info.prefix && info.executable && info.version)) { + traceError(`Incomplete pyenv environment info: ${JSON.stringify(info)}`); + return undefined; + } + + const versionsPath = normalizePath(path.join(path.dirname(path.dirname(pyenv)), 'versions')); + const envsPaths = normalizePath(path.join(path.dirname(versionsPath), 'envs')); + let group = undefined; + const normPrefix = normalizePath(info.prefix); + if (normPrefix.startsWith(versionsPath)) { + group = PYENV_VERSIONS; + } else if (normPrefix.startsWith(envsPaths)) { + group = PYENV_ENVIRONMENTS; + } + + const sv = shortVersion(info.version); + const name = info.name || info.displayName || path.basename(info.prefix); + let displayName = info.displayName || `pyenv (${sv})`; + if (info.kind === NativePythonEnvironmentKind.pyenvVirtualEnv) { + displayName = `${name} (${sv})`; + } + + const shellActivation: Map = new Map(); + const shellDeactivation: Map = new Map(); + + shellActivation.set('unknown', [{ executable: 'pyenv', args: ['shell', name] }]); + shellDeactivation.set('unknown', [{ executable: 'pyenv', args: ['shell', '--unset'] }]); + + const environment: PythonEnvironmentInfo = { + name: name, + displayName: displayName, + shortDisplayName: displayName, + displayPath: info.prefix, + version: info.version, + environmentPath: Uri.file(info.prefix), + description: undefined, + tooltip: info.prefix, + execInfo: { + run: { executable: info.executable }, + shellActivation, + shellDeactivation, + }, + sysPrefix: info.prefix, + group: group, + }; + + return api.createPythonEnvironmentItem(environment, manager); +} + +export async function refreshPyenv( + hardRefresh: boolean, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + traceInfo('Refreshing pyenv environments'); + const data = await nativeFinder.refresh(hardRefresh); + + let pyenv = await getPyenv(); + + if (pyenv === undefined) { + const managers = data + .filter((e) => !isNativeEnvInfo(e)) + .map((e) => e as NativeEnvManagerInfo) + .filter((e) => e.tool.toLowerCase() === 'pyenv'); + + if (managers.length > 0) { + pyenv = managers[0].executable; + await setPyenv(pyenv); + } + } + + const envs = data + .filter((e) => isNativeEnvInfo(e)) + .map((e) => e as NativeEnvInfo) + .filter( + (e) => + e.kind === NativePythonEnvironmentKind.pyenv || e.kind === NativePythonEnvironmentKind.pyenvVirtualEnv, + ); + + const collection: PythonEnvironment[] = []; + + envs.forEach((e) => { + if (pyenv) { + const environment = nativeToPythonEnv(e, api, manager, pyenv); + if (environment) { + collection.push(environment); + } + } + }); + + return sortEnvironments(collection); +} + +export async function resolvePyenvPath( + fsPath: string, + nativeFinder: NativePythonFinder, + api: PythonEnvironmentApi, + manager: EnvironmentManager, +): Promise { + try { + const e = await nativeFinder.resolve(fsPath); + if (e.kind !== NativePythonEnvironmentKind.pyenv) { + return undefined; + } + const pyenv = await getPyenv(nativeFinder); + if (!pyenv) { + traceError('Pyenv not found while resolving environment'); + return undefined; + } + + return nativeToPythonEnv(e, api, manager, pyenv); + } catch { + return undefined; + } +} diff --git a/src/managers/sysPython/main.ts b/src/managers/sysPython/main.ts deleted file mode 100644 index b4bb1c2..0000000 --- a/src/managers/sysPython/main.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Disposable, LogOutputChannel } from 'vscode'; -import { PythonEnvironmentApi } from '../../api'; -import { SysPythonManager } from './sysPythonManager'; -import { PipPackageManager } from './pipManager'; -import { VenvManager } from './venvManager'; -import { getPythonApi } from '../../features/pythonApi'; -import { NativePythonFinder } from '../common/nativePythonFinder'; -import { UvProjectCreator } from './uvProjectCreator'; -import { isUvInstalled } from './utils'; - -export async function registerSystemPythonFeatures( - nativeFinder: NativePythonFinder, - disposables: Disposable[], - log: LogOutputChannel, -): Promise { - const api: PythonEnvironmentApi = await getPythonApi(); - const envManager = new SysPythonManager(nativeFinder, api, log); - const venvManager = new VenvManager(nativeFinder, api, envManager, log); - const pkgManager = new PipPackageManager(api, log, venvManager); - - disposables.push( - api.registerPackageManager(pkgManager), - api.registerEnvironmentManager(envManager), - api.registerEnvironmentManager(venvManager), - ); - - setImmediate(async () => { - if (await isUvInstalled(log)) { - disposables.push(api.registerPythonProjectCreator(new UvProjectCreator(api, log))); - } - }); -} diff --git a/src/managers/sysPython/utils.ts b/src/managers/sysPython/utils.ts deleted file mode 100644 index aaa6ec3..0000000 --- a/src/managers/sysPython/utils.ts +++ /dev/null @@ -1,419 +0,0 @@ -import * as path from 'path'; -import { LogOutputChannel, QuickPickItem, Uri, window } from 'vscode'; -import { - EnvironmentManager, - Package, - PackageInstallOptions, - PackageManager, - PythonEnvironment, - PythonEnvironmentApi, - PythonEnvironmentInfo, - ResolveEnvironmentContext, -} from '../../api'; -import * as ch from 'child_process'; -import { ENVS_EXTENSION_ID, EXTENSION_ROOT_DIR } from '../../common/constants'; -import { - isNativeEnvInfo, - NativeEnvInfo, - NativePythonEnvironmentKind, - NativePythonFinder, -} from '../common/nativePythonFinder'; -import { createDeferred } from '../../common/utils/deferred'; -import { showErrorMessage } from '../../common/errors/utils'; -import { getWorkspacePersistentState } from '../../common/persistentState'; -import { shortVersion, sortEnvironments } from '../common/utils'; - -export const SYSTEM_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:system:WORKSPACE_SELECTED`; -export const SYSTEM_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:system:GLOBAL_SELECTED`; - -export async function clearSystemEnvCache(): Promise { - const keys = [SYSTEM_WORKSPACE_KEY, SYSTEM_GLOBAL_KEY]; - const state = await getWorkspacePersistentState(); - await state.clear(keys); -} - -export async function getSystemEnvForWorkspace(fsPath: string): Promise { - const state = await getWorkspacePersistentState(); - const data: { [key: string]: string } | undefined = await state.get(SYSTEM_WORKSPACE_KEY); - if (data) { - try { - return data[fsPath]; - } catch { - return undefined; - } - } - return undefined; -} - -export async function setSystemEnvForWorkspace(fsPath: string, envPath: string | undefined): Promise { - const state = await getWorkspacePersistentState(); - const data: { [key: string]: string } = (await state.get(SYSTEM_WORKSPACE_KEY)) ?? {}; - if (envPath) { - data[fsPath] = envPath; - } else { - delete data[fsPath]; - } - await state.set(SYSTEM_WORKSPACE_KEY, data); -} - -export async function getSystemEnvForGlobal(): Promise { - const state = await getWorkspacePersistentState(); - return await state.get(SYSTEM_GLOBAL_KEY); -} - -export async function setSystemEnvForGlobal(envPath: string | undefined): Promise { - const state = await getWorkspacePersistentState(); - await state.set(SYSTEM_GLOBAL_KEY, envPath); -} - -function asPackageQuickPickItem(name: string, version?: string): QuickPickItem { - return { - label: name, - description: version, - }; -} - -export async function pickPackages(uninstall: boolean, packages: string[] | Package[]): Promise { - const items = packages.map((pkg) => { - if (typeof pkg === 'string') { - return asPackageQuickPickItem(pkg); - } - return asPackageQuickPickItem(pkg.name, pkg.version); - }); - - const result = await window.showQuickPick(items, { - placeHolder: uninstall ? 'Select packages to uninstall' : 'Select packages to install', - canPickMany: true, - ignoreFocusOut: true, - }); - - if (Array.isArray(result)) { - return result.map((e) => e.label); - } - return []; -} - -const available = createDeferred(); -export async function isUvInstalled(log?: LogOutputChannel): Promise { - if (available.completed) { - return available.promise; - } - - const proc = ch.spawn('uv', ['--version']); - proc.on('error', () => { - available.resolve(false); - }); - proc.stdout.on('data', (d) => log?.info(d.toString())); - proc.on('exit', (code) => { - available.resolve(code === 0); - }); - return available.promise; -} - -export async function runUV(args: string[], cwd?: string, log?: LogOutputChannel): Promise { - log?.info(`Running: uv ${args.join(' ')}`); - return new Promise((resolve, reject) => { - const proc = ch.spawn('uv', args, { cwd: cwd }); - let builder = ''; - proc.stdout?.on('data', (data) => { - const s = data.toString('utf-8'); - builder += s; - log?.append(s); - }); - proc.stderr?.on('data', (data) => { - log?.append(data.toString('utf-8')); - }); - proc.on('close', () => { - resolve(builder); - }); - proc.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Failed to run python ${args.join(' ')}`)); - } - }); - }); -} - -export async function runPython(python: string, args: string[], cwd?: string, log?: LogOutputChannel): Promise { - log?.info(`Running: ${python} ${args.join(' ')}`); - return new Promise((resolve, reject) => { - const proc = ch.spawn(python, args, { cwd: cwd }); - let builder = ''; - proc.stdout?.on('data', (data) => { - const s = data.toString('utf-8'); - builder += s; - log?.append(`python: ${s}`); - }); - proc.stderr?.on('data', (data) => { - const s = data.toString('utf-8'); - builder += s; - log?.append(`python: ${s}`); - }); - proc.on('close', () => { - resolve(builder); - }); - proc.on('exit', (code) => { - if (code !== 0) { - reject(new Error(`Failed to run python ${args.join(' ')}`)); - } - }); - }); -} - -function getKindName(kind: NativePythonEnvironmentKind | undefined): string | undefined { - switch (kind) { - case NativePythonEnvironmentKind.homebrew: - return 'homebrew'; - - case NativePythonEnvironmentKind.macXCode: - return 'xcode'; - - case NativePythonEnvironmentKind.windowsStore: - return 'store'; - - case NativePythonEnvironmentKind.macCommandLineTools: - case NativePythonEnvironmentKind.macPythonOrg: - case NativePythonEnvironmentKind.globalPaths: - case NativePythonEnvironmentKind.linuxGlobal: - case NativePythonEnvironmentKind.windowsRegistry: - default: - return undefined; - } -} - -function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo { - if (env.executable && env.version && env.prefix) { - const kindName = getKindName(env.kind); - const sv = shortVersion(env.version); - const name = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`; - const displayName = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`; - const shortDisplayName = kindName ? `${sv} (${kindName})` : `${sv}`; - return { - name: env.name ?? name, - displayName: env.displayName ?? displayName, - shortDisplayName: shortDisplayName, - displayPath: env.executable, - version: env.version, - description: env.executable, - environmentPath: Uri.file(env.executable), - iconPath: Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')), - sysPrefix: env.prefix, - execInfo: { - run: { - executable: env.executable, - args: [], - }, - }, - }; - } else { - throw new Error(`Invalid python info: ${JSON.stringify(env)}`); - } -} - -export async function refreshPythons( - hardRefresh: boolean, - nativeFinder: NativePythonFinder, - api: PythonEnvironmentApi, - log: LogOutputChannel, - manager: EnvironmentManager, - uris?: Uri[], -): Promise { - const collection: PythonEnvironment[] = []; - const data = await nativeFinder.refresh(hardRefresh, uris); - const envs = data - .filter((e) => isNativeEnvInfo(e)) - .map((e) => e as NativeEnvInfo) - .filter( - (e) => - e.kind === undefined || - (e.kind && - [ - NativePythonEnvironmentKind.globalPaths, - NativePythonEnvironmentKind.homebrew, - NativePythonEnvironmentKind.linuxGlobal, - NativePythonEnvironmentKind.macCommandLineTools, - NativePythonEnvironmentKind.macPythonOrg, - NativePythonEnvironmentKind.macXCode, - NativePythonEnvironmentKind.windowsRegistry, - NativePythonEnvironmentKind.windowsStore, - ].includes(e.kind)), - ); - envs.forEach((env) => { - try { - const envInfo = getPythonInfo(env); - const python = api.createPythonEnvironmentItem(envInfo, manager); - collection.push(python); - } catch (e) { - log.error((e as Error).message); - } - }); - return sortEnvironments(collection); -} - -export async function refreshPackages( - environment: PythonEnvironment, - api: PythonEnvironmentApi, - manager: PackageManager, -): Promise { - if (!environment.execInfo) { - manager.logOutput?.error(`No executable found for python: ${environment.environmentPath.fsPath}`); - showErrorMessage(`No executable found for python: ${environment.environmentPath.fsPath}`, manager.logOutput); - return []; - } - - let data: string; - try { - const useUv = await isUvInstalled(); - if (useUv) { - data = await runUV( - ['pip', 'list', '--python', environment.execInfo.run.executable], - undefined, - manager.logOutput, - ); - } else { - data = await runPython( - environment.execInfo.run.executable, - ['-m', 'pip', 'list'], - undefined, - manager.logOutput, - ); - } - } catch (e) { - manager.logOutput?.error('Error refreshing packages', e); - showErrorMessage('Error refreshing packages', manager.logOutput); - return []; - } - - const collection: Package[] = []; - - const lines = data.split('\n').splice(2); - for (let line of lines) { - const parts = line.split(' ').filter((e) => e); - if (parts.length > 1) { - const name = parts[0].trim(); - const version = parts[1].trim(); - const pkg = api.createPackageItem( - { - name, - version, - displayName: name, - description: version, - }, - environment, - manager, - ); - collection.push(pkg); - } - } - return collection; -} - -export async function installPackages( - environment: PythonEnvironment, - packages: string[], - options: PackageInstallOptions, - api: PythonEnvironmentApi, - manager: PackageManager, -): Promise { - if (environment.execInfo) { - if (packages.length === 0) { - throw new Error('No packages selected to install'); - } - - const useUv = await isUvInstalled(); - - const installArgs = ['pip', 'install']; - if (options.upgrade) { - installArgs.push('--upgrade'); - } - if (useUv) { - await runUV( - [...installArgs, '--python', environment.execInfo.run.executable, ...packages], - undefined, - manager.logOutput, - ); - } else { - await runPython( - environment.execInfo.run.executable, - ['-m', ...installArgs, ...packages], - undefined, - manager.logOutput, - ); - } - - return refreshPackages(environment, api, manager); - } - throw new Error(`No executable found for python: ${environment.environmentPath.fsPath}`); -} - -export async function uninstallPackages( - environment: PythonEnvironment, - api: PythonEnvironmentApi, - manager: PackageManager, - packages: string[] | Package[], -): Promise { - if (environment.execInfo) { - const remove = []; - for (let pkg of packages) { - if (typeof pkg === 'string') { - remove.push(pkg); - } else { - remove.push(pkg.name); - } - } - if (remove.length === 0) { - const installed = await manager.getPackages(environment); - if (installed) { - const packages = await pickPackages(true, installed); - if (packages.length === 0) { - throw new Error('No packages selected to uninstall'); - } - } - } - - const useUv = await isUvInstalled(); - if (useUv) { - await runUV( - ['pip', 'uninstall', '--python', environment.execInfo.run.executable, ...remove], - undefined, - manager.logOutput, - ); - } else { - await runPython( - environment.execInfo.run.executable, - ['-m', 'pip', 'uninstall', '-y', ...remove], - undefined, - manager.logOutput, - ); - } - return refreshPackages(environment, api, manager); - } - throw new Error(`No executable found for python: ${environment.environmentPath.fsPath}`); -} - -export async function resolvePythonEnvironment( - context: ResolveEnvironmentContext, - nativeFinder: NativePythonFinder, - - api: PythonEnvironmentApi, - manager: EnvironmentManager, -): Promise { - const fsPath = context instanceof Uri ? context.fsPath : context.environmentPath.fsPath; - const resolved = await resolvePythonEnvironmentPath(fsPath, nativeFinder, api, manager); - return resolved; -} - -export async function resolvePythonEnvironmentPath( - fsPath: string, - nativeFinder: NativePythonFinder, - api: PythonEnvironmentApi, - manager: EnvironmentManager, -): Promise { - const resolved = await nativeFinder.resolve(fsPath); - - // This is supposed to handle a python interpreter as long as we know some basic things about it - if (resolved.executable && resolved.version && resolved.prefix) { - const envInfo = getPythonInfo(resolved); - return api.createPythonEnvironmentItem(envInfo, manager); - } -} diff --git a/src/managers/sysPython/uvProjectCreator.ts b/src/managers/sysPython/uvProjectCreator.ts deleted file mode 100644 index 328d302..0000000 --- a/src/managers/sysPython/uvProjectCreator.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { LogOutputChannel, MarkdownString, ProgressLocation, window } from 'vscode'; -import { - IconPath, - PythonEnvironmentApi, - PythonProject, - PythonProjectCreator, - PythonProjectCreatorOptions, -} from '../../api'; -import { pickProject } from '../../common/pickers'; -import { runUV } from './utils'; - -export class UvProjectCreator implements PythonProjectCreator { - constructor(private readonly api: PythonEnvironmentApi, private log: LogOutputChannel) { - this.name = 'uv'; - this.displayName = 'UV Init'; - this.description = 'Initialize a Python project using UV'; - this.tooltip = new MarkdownString('Initialize a Python Project using `uv init`'); - } - - readonly name: string; - readonly displayName?: string; - readonly description?: string; - readonly tooltip?: string | MarkdownString; - readonly iconPath?: IconPath; - - public async create(_option?: PythonProjectCreatorOptions): Promise { - const projectName = await window.showInputBox({ - prompt: 'Enter the name of the project', - value: 'myproject', - ignoreFocusOut: true, - }); - - if (!projectName) { - return; - } - - const projectPath = await pickProject(this.api.getPythonProjects()); - - if (!projectPath) { - return; - } - - const projectType = ( - await window.showQuickPick( - [ - { label: 'Library', description: 'Create a Python library project', detail: '--lib' }, - { label: 'Application', description: 'Create a Python application project', detail: '--app' }, - ], - { - placeHolder: 'Select the type of project to create', - ignoreFocusOut: true, - }, - ) - )?.detail; - - if (!projectType) { - return; - } - - // --package Set up the project to be built as a Python package - // --no-package Do not set up the project to be built as a Python package - // --no-readme Do not create a `README.md` file - // --no-pin-python Do not create a `.python-version` file for the project - // --no-workspace Avoid discovering a workspace and create a standalone project - const projectOptions = - ( - await window.showQuickPick( - [ - { - label: 'Package', - description: 'Set up the project to be built as a Python package', - detail: '--package', - }, - { - label: 'No Package', - description: 'Do not set up the project to be built as a Python package', - detail: '--no-package', - }, - { label: 'No Readme', description: 'Do not create a `README.md` file', detail: '--no-readme' }, - { - label: 'No Pin Python', - description: 'Do not create a `.python-version` file for the project', - detail: '--no-pin-python', - }, - { - label: 'No Workspace', - description: 'Avoid discovering a workspace and create a standalone project', - detail: '--no-workspace', - }, - ], - { - placeHolder: 'Select the options for the project', - ignoreFocusOut: true, - canPickMany: true, - }, - ) - )?.map((item) => item.detail) ?? []; - try { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Creating project', - }, - async () => { - await runUV( - ['init', projectType, '--name', projectName, ...projectOptions, projectPath.uri.fsPath], - undefined, - this.log, - ); - }, - ); - } catch { - const result = await window.showErrorMessage('Failed to create project', 'View Output'); - if (result === 'View Output') { - this.log.show(); - } - return; - } - - return undefined; - } -} diff --git a/src/managers/sysPython/venvManager.ts b/src/managers/sysPython/venvManager.ts deleted file mode 100644 index 386f368..0000000 --- a/src/managers/sysPython/venvManager.ts +++ /dev/null @@ -1,351 +0,0 @@ -import { ProgressLocation, Uri, window, LogOutputChannel, EventEmitter, MarkdownString } from 'vscode'; -import { - CreateEnvironmentScope, - DidChangeEnvironmentEventArgs, - DidChangeEnvironmentsEventArgs, - EnvironmentChangeKind, - EnvironmentManager, - GetEnvironmentScope, - GetEnvironmentsScope, - IconPath, - PythonEnvironment, - PythonEnvironmentApi, - PythonProject, - RefreshEnvironmentsScope, - ResolveEnvironmentContext, - SetEnvironmentScope, -} from '../../api'; -import { - createPythonVenv, - findVirtualEnvironments, - getVenvForGlobal, - getVenvForWorkspace, - removeVenv, - setVenvForGlobal, - setVenvForWorkspace, -} from './venvUtils'; -import * as path from 'path'; -import { resolvePythonEnvironment, resolvePythonEnvironmentPath } from './utils'; -import { NativePythonFinder } from '../common/nativePythonFinder'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { pickProject } from '../../common/pickers'; -import { createDeferred, Deferred } from '../../common/utils/deferred'; -import { getLatest, sortEnvironments } from '../common/utils'; - -export class VenvManager implements EnvironmentManager { - private collection: PythonEnvironment[] = []; - private readonly fsPathToEnv: Map = new Map(); - private globalEnv: PythonEnvironment | undefined; - - private readonly _onDidChangeEnvironment = new EventEmitter(); - public readonly onDidChangeEnvironment = this._onDidChangeEnvironment.event; - - private readonly _onDidChangeEnvironments = new EventEmitter(); - public readonly onDidChangeEnvironments = this._onDidChangeEnvironments.event; - - readonly name: string; - readonly displayName?: string | undefined; - readonly preferredPackageManagerId: string; - readonly description?: string | undefined; - readonly tooltip?: string | MarkdownString | undefined; - readonly iconPath?: IconPath | undefined; - - constructor( - private readonly nativeFinder: NativePythonFinder, - private readonly api: PythonEnvironmentApi, - private readonly baseManager: EnvironmentManager, - public readonly log: LogOutputChannel, - ) { - this.name = 'venv'; - this.displayName = 'venv Environments'; - this.description = 'Manages virtual environments created using venv'; - this.tooltip = new MarkdownString('Manages virtual environments created using `venv`', true); - this.preferredPackageManagerId = 'ms-python.python:pip'; - this.iconPath = Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')); - } - - private _initialized: Deferred | undefined; - async initialize(): Promise { - if (this._initialized) { - return this._initialized.promise; - } - - this._initialized = createDeferred(); - - try { - await this.internalRefresh(undefined, false, 'Initializing virtual environments'); - } finally { - this._initialized.resolve(); - } - } - - async create(scope: CreateEnvironmentScope): Promise { - const project = scope === 'global' ? await pickProject(this.api.getPythonProjects()) : scope; - if (!project) { - return; - } - - const globals = await this.baseManager.getEnvironments('global'); - const environment = await createPythonVenv(this.nativeFinder, this.api, this.log, this, globals, project); - if (environment) { - this.collection.push(environment); - this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.add }]); - } - return environment; - } - - async remove(environment: PythonEnvironment): Promise { - await removeVenv(environment, this.log); - this.updateCollection(environment); - this._onDidChangeEnvironments.fire([{ environment, kind: EnvironmentChangeKind.remove }]); - - const changedUris = this.updateFsPathToEnv(environment); - - for (const uri of changedUris) { - const newEnv = await this.get(uri); - this._onDidChangeEnvironment.fire({ uri, old: environment, new: newEnv }); - } - } - - private updateCollection(environment: PythonEnvironment): void { - this.collection = this.collection.filter( - (e) => e.environmentPath.fsPath !== environment.environmentPath.fsPath, - ); - } - - private updateFsPathToEnv(environment: PythonEnvironment): Uri[] { - const changed: Uri[] = []; - this.fsPathToEnv.forEach((env, uri) => { - if (env.environmentPath.fsPath === environment.environmentPath.fsPath) { - this.fsPathToEnv.delete(uri); - changed.push(Uri.file(uri)); - } - }); - return changed; - } - - async refresh(scope: RefreshEnvironmentsScope): Promise { - return this.internalRefresh(scope, true, 'Refreshing virtual environments'); - } - - private async internalRefresh(scope: RefreshEnvironmentsScope, hardRefresh: boolean, title: string): Promise { - await window.withProgress( - { - location: ProgressLocation.Window, - title, - }, - async () => { - const discard = this.collection.map((env) => ({ - kind: EnvironmentChangeKind.remove, - environment: env, - })); - - this.collection = await findVirtualEnvironments( - hardRefresh, - this.nativeFinder, - this.api, - this.log, - this, - scope ? [scope] : this.api.getPythonProjects().map((p) => p.uri), - ); - await this.loadEnvMap(); - - const added = this.collection.map((env) => ({ environment: env, kind: EnvironmentChangeKind.add })); - this._onDidChangeEnvironments.fire([...discard, ...added]); - }, - ); - } - - async getEnvironments(scope: GetEnvironmentsScope): Promise { - await this.initialize(); - - if (scope === 'all') { - return Array.from(this.collection); - } - if (!(scope instanceof Uri)) { - return []; - } - - const env = this.fsPathToEnv.get(scope.fsPath); - return env ? [env] : []; - } - - async get(scope: GetEnvironmentScope): Promise { - await this.initialize(); - - if (!scope) { - // `undefined` for venv scenario return the global environment. - return this.globalEnv; - } - - const project = this.api.getPythonProject(scope); - if (!project) { - return this.globalEnv; - } - - let env = this.fsPathToEnv.get(project.uri.fsPath) ?? this.globalEnv; - if (!env) { - env = this.findEnvironmentByPath(project.uri.fsPath); - } - - return env; - } - - async set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise { - if (scope === undefined) { - this.globalEnv = environment; - if (environment) { - await setVenvForGlobal(environment.environmentPath.fsPath); - } - return; - } - - if (scope instanceof Uri) { - const pw = this.api.getPythonProject(scope); - if (!pw) { - return; - } - - if (environment) { - const before = this.fsPathToEnv.get(pw.uri.fsPath); - this.fsPathToEnv.set(pw.uri.fsPath, environment); - await setVenvForWorkspace(pw.uri.fsPath, environment.environmentPath.fsPath); - this._onDidChangeEnvironment.fire({ uri: scope, old: before, new: environment }); - } else { - this.fsPathToEnv.delete(pw.uri.fsPath); - await setVenvForWorkspace(pw.uri.fsPath, undefined); - } - } - } - - async resolve(context: ResolveEnvironmentContext): Promise { - if (context instanceof Uri) { - // NOTE: `environmentPath` for envs in `this.collection` for venv always points to the python - // executable in the venv. This is set when we create the PythonEnvironment object. - const found = this.findEnvironmentByPath(context.fsPath); - if (found) { - // If it is in the collection, then it is a venv, and it should already be fully resolved. - return found; - } - } else { - // We have received a partially or fully resolved environment. - const found = - this.collection.find((e) => e.envId.id === context.envId.id) ?? - this.findEnvironmentByPath(context.environmentPath.fsPath); - if (found) { - // If it is in the collection, then it is a venv, and it should already be fully resolved. - return found; - } - - if (context.execInfo) { - // This is a fully resolved environment, from venv perspective. - return context; - } - } - - const resolved = await resolvePythonEnvironment(context, this.nativeFinder, this.api, this); - if (resolved) { - // This is just like finding a new environment or creating a new one. - // Add it to collection, and trigger the added event. - this.collection.push(resolved); - this._onDidChangeEnvironments.fire([{ environment: resolved, kind: EnvironmentChangeKind.add }]); - } - - return resolved; - } - - private async loadEnvMap() { - this.globalEnv = undefined; - this.fsPathToEnv.clear(); - - const globals = await this.baseManager.getEnvironments('global'); - // Try to find a global environment - const fsPath = await getVenvForGlobal(); - - if (fsPath) { - this.globalEnv = this.findEnvironmentByPath(fsPath) ?? this.findEnvironmentByPath(fsPath, globals); - - // If the environment is not found, resolve the fsPath. Could be portable conda. - if (!this.globalEnv) { - this.globalEnv = await resolvePythonEnvironmentPath(fsPath, this.nativeFinder, this.api, this); - - // If the environment is resolved, add it to the collection - if (this.globalEnv) { - this.collection.push(this.globalEnv); - } - } - } - - // If a global environment is still not set, use latest from globals - if (!this.globalEnv) { - this.globalEnv = getLatest(globals); - } - - const sorted = sortEnvironments(this.collection); - const paths = this.api.getPythonProjects().map((p) => path.normalize(p.uri.fsPath)); - for (const p of paths) { - const env = await getVenvForWorkspace(p); - - if (env) { - const found = this.findEnvironmentByPath(env, sorted) ?? this.findEnvironmentByPath(env, globals); - const previous = this.fsPathToEnv.get(p); - const pw = this.api.getPythonProject(Uri.file(p)); - if (found) { - this.fsPathToEnv.set(p, found); - if (pw && previous?.envId.id !== found.envId.id) { - this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: found }); - } - } else { - const resolved = await resolvePythonEnvironmentPath(env, this.nativeFinder, this.api, this); - if (resolved) { - // If resolved add it to the collection - this.fsPathToEnv.set(p, resolved); - this.collection.push(resolved); - if (pw && previous?.envId.id !== resolved.envId.id) { - this._onDidChangeEnvironment.fire({ uri: pw.uri, old: undefined, new: resolved }); - } - } else { - this.log.error(`Failed to resolve python environment: ${env}`); - } - } - } else { - // There is NO selected venv, then try and choose the venv that is in the workspace. - if (sorted.length === 1) { - this.fsPathToEnv.set(p, sorted[0]); - } else { - // These are sorted by version and by path length. The assumption is that the user would want to pick - // latest version and the one that is closest to the workspace. - const found = sorted.find((e) => { - const t = this.api.getPythonProject(e.environmentPath)?.uri.fsPath; - return t && path.normalize(t) === p; - }); - if (found) { - this.fsPathToEnv.set(p, found); - } - } - } - } - } - - private findEnvironmentByPath(fsPath: string, collection?: PythonEnvironment[]): PythonEnvironment | undefined { - const normalized = path.normalize(fsPath); - const envs = collection ?? this.collection; - return envs.find((e) => { - const n = path.normalize(e.environmentPath.fsPath); - return n === normalized || path.dirname(n) === normalized || path.dirname(path.dirname(n)) === normalized; - }); - } - - public getProjectsByEnvironment(environment: PythonEnvironment): PythonProject[] { - const projects: PythonProject[] = []; - this.fsPathToEnv.forEach((env, fsPath) => { - if (env.envId.id === environment.envId.id) { - const p = this.api.getPythonProject(Uri.file(fsPath)); - if (p) { - projects.push(p); - } - } - }); - return projects; - } -} diff --git a/src/managers/sysPython/venvUtils.ts b/src/managers/sysPython/venvUtils.ts deleted file mode 100644 index df1ed97..0000000 --- a/src/managers/sysPython/venvUtils.ts +++ /dev/null @@ -1,377 +0,0 @@ -import { LogOutputChannel, ProgressLocation, RelativePattern, Uri, window } from 'vscode'; -import { - EnvironmentManager, - Installable, - PythonCommandRunConfiguration, - PythonEnvironment, - PythonEnvironmentApi, - PythonProject, - TerminalShellType, -} from '../../api'; -import * as tomljs from '@iarna/toml'; -import * as path from 'path'; -import * as os from 'os'; -import * as fsapi from 'fs-extra'; -import { isUvInstalled, runPython, runUV } from './utils'; -import { ENVS_EXTENSION_ID, EXTENSION_ROOT_DIR } from '../../common/constants'; -import { - isNativeEnvInfo, - NativeEnvInfo, - NativePythonEnvironmentKind, - NativePythonFinder, -} from '../common/nativePythonFinder'; -import { pickEnvironmentFrom } from '../../common/pickers'; -import { getWorkspacePersistentState } from '../../common/persistentState'; -import { shortVersion, sortEnvironments } from '../common/utils'; -import { findFiles } from '../../common/workspace.apis'; - -export const VENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:venv:WORKSPACE_SELECTED`; -export const VENV_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:venv:GLOBAL_SELECTED`; - -export async function clearVenvCache(): Promise { - const keys = [VENV_WORKSPACE_KEY, VENV_GLOBAL_KEY]; - const state = await getWorkspacePersistentState(); - await state.clear(keys); -} - -export async function getVenvForWorkspace(fsPath: string): Promise { - const state = await getWorkspacePersistentState(); - const data: { [key: string]: string } | undefined = await state.get(VENV_WORKSPACE_KEY); - if (data) { - try { - return data[fsPath]; - } catch { - return undefined; - } - } - return undefined; -} - -export async function setVenvForWorkspace(fsPath: string, envPath: string | undefined): Promise { - const state = await getWorkspacePersistentState(); - const data: { [key: string]: string } = (await state.get(VENV_WORKSPACE_KEY)) ?? {}; - if (envPath) { - data[fsPath] = envPath; - } else { - delete data[fsPath]; - } - await state.set(VENV_WORKSPACE_KEY, data); -} - -export async function getVenvForGlobal(): Promise { - const state = await getWorkspacePersistentState(); - return await state.get(VENV_GLOBAL_KEY); -} - -export async function setVenvForGlobal(envPath: string | undefined): Promise { - const state = await getWorkspacePersistentState(); - await state.set(VENV_GLOBAL_KEY, envPath); -} - -function getName(binPath: string): string { - const dir1 = path.dirname(binPath); - if (dir1.endsWith('bin') || dir1.endsWith('Scripts') || dir1.endsWith('scripts')) { - return path.basename(path.dirname(dir1)); - } - return path.basename(dir1); -} - -export async function findVirtualEnvironments( - hardRefresh: boolean, - nativeFinder: NativePythonFinder, - api: PythonEnvironmentApi, - log: LogOutputChannel, - manager: EnvironmentManager, - uris?: Uri[], -): Promise { - const collection: PythonEnvironment[] = []; - const data = await nativeFinder.refresh(hardRefresh, uris); - const envs = data - .filter((e) => isNativeEnvInfo(e)) - .map((e) => e as NativeEnvInfo) - .filter((e) => e.kind === NativePythonEnvironmentKind.venv); - - envs.forEach((e) => { - if (!(e.prefix && e.executable && e.version)) { - log.warn(`Invalid conda environment: ${JSON.stringify(e)}`); - return; - } - - const venvName = e.name ?? getName(e.executable); - const sv = shortVersion(e.version); - const name = `${venvName} (${sv})`; - - const binDir = path.dirname(e.executable); - - const shellActivation: Map = new Map(); - shellActivation.set(TerminalShellType.bash, [{ executable: 'source', args: [path.join(binDir, 'activate')] }]); - shellActivation.set(TerminalShellType.powershell, [{ executable: path.join(binDir, 'Activate.ps1') }]); - shellActivation.set(TerminalShellType.commandPrompt, [{ executable: path.join(binDir, 'activate.bat') }]); - shellActivation.set(TerminalShellType.unknown, [{ executable: path.join(binDir, 'activate') }]); - - const shellDeactivation = new Map(); - shellDeactivation.set(TerminalShellType.bash, [{ executable: 'deactivate' }]); - shellDeactivation.set(TerminalShellType.powershell, [{ executable: 'deactivate' }]); - shellDeactivation.set(TerminalShellType.commandPrompt, [{ executable: path.join(binDir, 'deactivate.bat') }]); - shellActivation.set(TerminalShellType.unknown, [{ executable: 'deactivate' }]); - - const env = api.createPythonEnvironmentItem( - { - name: name, - displayName: name, - shortDisplayName: `${sv} (${venvName})`, - displayPath: e.executable, - version: e.version, - description: e.executable, - environmentPath: Uri.file(e.executable), - iconPath: Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')), - sysPrefix: e.prefix, - execInfo: { - run: { - executable: e.executable, - }, - activatedRun: { - executable: e.executable, - }, - shellActivation, - shellDeactivation, - }, - }, - manager, - ); - collection.push(env); - log.info(`Found venv environment: ${name}`); - }); - return collection; -} - -export async function createPythonVenv( - nativeFinder: NativePythonFinder, - api: PythonEnvironmentApi, - log: LogOutputChannel, - manager: EnvironmentManager, - basePythons: PythonEnvironment[], - project: PythonProject, -): Promise { - const filtered = basePythons.filter((e) => e.execInfo); - if (filtered.length === 0) { - log.error('No base python found'); - window.showErrorMessage('No base python found'); - return; - } - - const basePython = await pickEnvironmentFrom(sortEnvironments(filtered)); - if (!basePython || !basePython.execInfo) { - log.error('No base python selected, cannot create virtual environment.'); - window.showErrorMessage('No base python selected, cannot create virtual environment.'); - return; - } - - const name = await window.showInputBox({ - prompt: 'Enter name for virtual environment', - value: '.venv', - ignoreFocusOut: true, - }); - if (!name) { - log.error('No name entered, cannot create virtual environment.'); - window.showErrorMessage('No name entered, cannot create virtual environment.'); - return; - } - - const envPath = path.join(project.uri.fsPath, name); - const pythonPath = - os.platform() === 'win32' ? path.join(envPath, 'Scripts', 'python.exe') : path.join(envPath, 'bin', 'python'); - - return await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Creating virtual environment', - }, - async () => { - try { - const useUv = await isUvInstalled(log); - if (basePython.execInfo?.run.executable) { - if (useUv) { - await runUV( - ['venv', '--verbose', '--seed', '--python', basePython.execInfo?.run.executable, name], - project.uri.fsPath, - log, - ); - } else { - await runPython( - basePython.execInfo.run.executable, - ['-m', 'venv', name], - project.uri.fsPath, - manager.log, - ); - } - if (!(await fsapi.pathExists(pythonPath))) { - log.error('no python executable found in virtual environment'); - throw new Error('no python executable found in virtual environment'); - } - } - - const resolved = await nativeFinder.resolve(pythonPath); - if (resolved.version && resolved.executable && resolved.prefix) { - const sv = shortVersion(resolved.version); - const env = api.createPythonEnvironmentItem( - { - name: `${name} (${sv})`, - displayName: `${name} (${sv})`, - shortDisplayName: `${name}:${sv}`, - displayPath: pythonPath, - version: resolved.version, - description: pythonPath, - environmentPath: Uri.file(pythonPath), - iconPath: Uri.file(path.join(EXTENSION_ROOT_DIR, 'files', 'logo.svg')), - sysPrefix: resolved.prefix, - execInfo: { - run: { - executable: pythonPath, - args: [], - }, - }, - }, - manager, - ); - log.info(`Created venv environment: ${name}`); - return env; - } else { - throw new Error('Could not resolve the virtual environment'); - } - } catch (e) { - log.error(`Failed to create virtual environment: ${e}`); - window.showErrorMessage(`Failed to create virtual environment`); - return; - } - }, - ); -} - -export async function removeVenv(environment: PythonEnvironment, log: LogOutputChannel): Promise { - const pythonPath = os.platform() === 'win32' ? 'python.exe' : 'python'; - - const envPath = environment.environmentPath.fsPath.endsWith(pythonPath) - ? path.dirname(path.dirname(environment.environmentPath.fsPath)) - : environment.environmentPath.fsPath; - - const confirm = await window.showWarningMessage(`Are you sure you want to remove ${envPath}?`, 'Yes', 'No'); - if (confirm === 'Yes') { - await window.withProgress( - { - location: ProgressLocation.Notification, - title: 'Removing virtual environment', - }, - async () => { - try { - await fsapi.remove(envPath); - return true; - } catch (e) { - log.error(`Failed to remove virtual environment: ${e}`); - return false; - } - }, - ); - } - - return false; -} - -function tomlParse(content: string, log?: LogOutputChannel): tomljs.JsonMap { - try { - return tomljs.parse(content); - } catch (err) { - log?.error('Failed to parse `pyproject.toml`:', err); - } - return {}; -} - -function isPipInstallableToml(toml: tomljs.JsonMap): boolean { - return toml['build-system'] !== undefined && toml.project !== undefined; -} - -function getTomlInstallable(toml: tomljs.JsonMap, tomlPath: Uri): Installable[] { - const extras: Installable[] = []; - - if (isPipInstallableToml(toml)) { - extras.push({ - displayName: 'Editable', - description: 'Install project as editable', - group: 'Toml', - args: ['-e', path.dirname(tomlPath.fsPath)], - uri: tomlPath, - }); - } - - if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { - const deps = (toml.project as tomljs.JsonMap)['optional-dependencies']; - for (const key of Object.keys(deps)) { - extras.push({ - displayName: key, - group: 'Toml', - args: ['-e', `.[${key}]`], - uri: tomlPath, - }); - } - } - return extras; -} - -export async function getProjectInstallable( - api: PythonEnvironmentApi, - project?: PythonProject, -): Promise { - if (!project) { - return []; - } - const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; - const installable: Installable[] = []; - await window.withProgress( - { - location: ProgressLocation.Window, - title: 'Searching dependencies', - }, - async (progress, token) => { - progress.report({ message: 'Searching for requirements files' }); - const results1 = await findFiles( - new RelativePattern(project.uri, '**/*requirements*.txt'), - exclude, - undefined, - token, - ); - const results2 = await findFiles( - new RelativePattern(project.uri, '**/requirements/*.txt'), - exclude, - undefined, - token, - ); - [...results1, ...results2].forEach((uri) => { - const p = api.getPythonProject(uri); - if (p?.uri.fsPath === project.uri.fsPath) { - installable.push({ - uri, - displayName: path.basename(uri.fsPath), - group: 'requirements', - args: ['-r', uri.fsPath], - }); - } - }); - - progress.report({ message: 'Searching for `pyproject.toml` file' }); - const results3 = await findFiles( - new RelativePattern(project.uri, '**/pyproject.toml'), - exclude, - undefined, - token, - ); - results3.filter((uri) => api.getPythonProject(uri)?.uri.fsPath === project.uri.fsPath); - await Promise.all( - results3.map(async (uri) => { - const toml = tomlParse(await fsapi.readFile(uri.fsPath, 'utf-8')); - installable.push(...getTomlInstallable(toml, uri)); - }), - ); - }, - ); - return installable; -} diff --git a/src/test/common/environmentPicker.unit.test.ts b/src/test/common/environmentPicker.unit.test.ts new file mode 100644 index 0000000..dc0ee3c --- /dev/null +++ b/src/test/common/environmentPicker.unit.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import assert from 'node:assert'; +import { Uri } from 'vscode'; +import { PythonEnvironment } from '../../api'; + +/** + * Test the logic used in environment pickers to include interpreter paths in descriptions + */ +suite('Environment Picker Description Logic', () => { + const createMockEnvironment = ( + displayPath: string, + description?: string, + name: string = 'Python 3.9.0', + ): PythonEnvironment => ({ + envId: { id: 'test', managerId: 'test-manager' }, + name, + displayName: name, + displayPath, + version: '3.9.0', + environmentPath: Uri.file(displayPath), + description, + sysPrefix: '/path/to/prefix', + execInfo: { run: { executable: displayPath } }, + }); + + suite('Description formatting with interpreter path', () => { + test('should use displayPath as description when no original description exists', () => { + const env = createMockEnvironment('/usr/local/bin/python'); + + // This is the logic from our updated picker + const pathDescription = env.displayPath; + const description = + env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; + + assert.strictEqual(description, '/usr/local/bin/python'); + }); + + test('should append displayPath to existing description in parentheses', () => { + const env = createMockEnvironment('/home/user/.venv/bin/python', 'Virtual Environment'); + + // This is the logic from our updated picker + const pathDescription = env.displayPath; + const description = + env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; + + assert.strictEqual(description, 'Virtual Environment (/home/user/.venv/bin/python)'); + }); + + test('should handle complex paths correctly', () => { + const complexPath = '/usr/local/anaconda3/envs/my-project-env/bin/python'; + const env = createMockEnvironment(complexPath, 'Conda Environment'); + + // This is the logic from our updated picker + const pathDescription = env.displayPath; + const description = + env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; + + assert.strictEqual(description, `Conda Environment (${complexPath})`); + }); + + test('should handle empty description correctly', () => { + const env = createMockEnvironment('/opt/python/bin/python', ''); + + // This is the logic from our updated picker + const pathDescription = env.displayPath; + const description = + env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; + + // Empty string should be treated like no description, so just use path + assert.strictEqual(description, '/opt/python/bin/python'); + }); + + test('should handle Windows paths correctly', () => { + const windowsPath = 'C:\\Python39\\python.exe'; + const env = createMockEnvironment(windowsPath, 'System Python'); + + // This is the logic from our updated picker + const pathDescription = env.displayPath; + const description = + env.description && env.description.trim() ? `${env.description} (${pathDescription})` : pathDescription; + + assert.strictEqual(description, 'System Python (C:\\Python39\\python.exe)'); + }); + }); +}); diff --git a/src/test/common/internalVariables.unit.test.ts b/src/test/common/internalVariables.unit.test.ts new file mode 100644 index 0000000..f9ed6b6 --- /dev/null +++ b/src/test/common/internalVariables.unit.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert'; +import { resolveVariables } from '../../common/utils/internalVariables'; +import * as workspaceApi from '../../common/workspace.apis'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; + +suite('Internal Variable substitution', () => { + let getWorkspaceFolderStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + + const home = process.env.HOME ?? process.env.USERPROFILE ?? ''; + const project = { name: 'project1', uri: { fsPath: path.join(home, 'workspace1', 'project1') } }; + const workspaceFolder = { name: 'workspace1', uri: { fsPath: path.join(home, 'workspace1') } }; + + setup(() => { + getWorkspaceFolderStub = sinon.stub(workspaceApi, 'getWorkspaceFolder'); + getWorkspaceFoldersStub = sinon.stub(workspaceApi, 'getWorkspaceFolders'); + + getWorkspaceFolderStub.returns(workspaceFolder); + getWorkspaceFoldersStub.returns([workspaceFolder]); + }); + + teardown(() => { + sinon.restore(); + }); + + [ + { variable: '${userHome}', substitution: home }, + { variable: '${pythonProject}', substitution: project.uri.fsPath }, + { variable: '${workspaceFolder}', substitution: workspaceFolder.uri.fsPath }, + { variable: '${workspaceFolder:workspace1}', substitution: workspaceFolder.uri.fsPath }, + { variable: '${cwd}', substitution: process.cwd() }, + process.platform === 'win32' + ? { variable: '${env:USERPROFILE}', substitution: home } + : { variable: '${env:HOME}', substitution: home }, + ].forEach((item) => { + test(`Resolve ${item.variable}`, () => { + // Two times here to ensure that both instances are handled + const value = `Some ${item.variable} text ${item.variable}`; + const result = resolveVariables(value, project.uri as unknown as Uri); + assert.equal(result, `Some ${item.substitution} text ${item.substitution}`); + }); + }); +}); diff --git a/src/test/common/pathUtils.unit.test.ts b/src/test/common/pathUtils.unit.test.ts new file mode 100644 index 0000000..1733e78 --- /dev/null +++ b/src/test/common/pathUtils.unit.test.ts @@ -0,0 +1,131 @@ +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { getResourceUri, normalizePath } from '../../common/utils/pathUtils'; +import * as utils from '../../common/utils/platformUtils'; + +suite('Path Utilities', () => { + suite('getResourceUri', () => { + const testRoot = process.cwd(); + + test('returns undefined when path is empty', () => { + const result = getResourceUri('', testRoot); + assert.strictEqual(result, undefined); + }); + + test('returns undefined when path is undefined', () => { + // @ts-ignore: Testing with undefined even though the type doesn't allow it + const result = getResourceUri(undefined, testRoot); + assert.strictEqual(result, undefined); + }); + test('creates file URI from normal file path', () => { + const testPath = '/path/to/file.txt'; + const result = getResourceUri(testPath, testRoot); + + assert.ok(result instanceof Uri); + assert.strictEqual(result?.scheme, 'file'); + assert.strictEqual(result?.path, testPath); + }); + + test('creates file URI from Windows path', function () { + if (!utils.isWindows()) { + this.skip(); + } + const testPath = 'C:\\path\\to\\file.txt'; + const result = getResourceUri(testPath, testRoot); + + assert.ok(result instanceof Uri); + assert.strictEqual(result?.scheme, 'file'); + assert.strictEqual(result?.path, '/C:/path/to/file.txt'); + }); + + test('parses existing URI correctly', () => { + const uriString = 'scheme://authority/path'; + const result = getResourceUri(uriString, testRoot); + + assert.ok(result instanceof Uri); + assert.strictEqual(result?.scheme, 'scheme'); + assert.strictEqual(result?.authority, 'authority'); + assert.strictEqual(result?.path, '/path'); + }); + + test('handles exception and returns undefined', () => { + // Create a scenario that would cause an exception + // For this test, we'll mock Uri.file to throw an error + const originalUriFile = Uri.file; + Uri.file = () => { + throw new Error('Test error'); + }; + try { + const result = getResourceUri('some-path', testRoot); + assert.strictEqual(result, undefined); + } finally { + // Restore the original function + Uri.file = originalUriFile; + } + }); + + test('handles relative paths by resolving against the provided root', () => { + const path = require('path'); + + // Use a relative path + const relativePath = './relative/path/file.txt'; + const customRoot = path.join(testRoot, 'custom/root'); + + const result = getResourceUri(relativePath, customRoot); + + assert.ok(result instanceof Uri); + assert.strictEqual(result?.scheme, 'file'); + // The resulting path should be resolved against the custom root + assert.ok( + result!.fsPath.replace(/\\/g, '/').toLowerCase().endsWith('relative/path/file.txt'), + `Expected path to end with the relative path segment, but got: ${result!.fsPath}`, + ); + + // Verify the path contains the custom root + const normalizedResult = result!.fsPath.replace(/\\/g, '/').toLowerCase(); + const normalizedRoot = customRoot.replace(/\\/g, '/').toLowerCase(); + assert.ok( + normalizedResult.includes(normalizedRoot), + `Expected path to include the custom root "${normalizedRoot}", but got: ${normalizedResult}`, + ); + }); + }); + + suite('normalizePath', () => { + let isWindowsStub: sinon.SinonStub; + + setup(() => { + isWindowsStub = sinon.stub(utils, 'isWindows'); + }); + + teardown(() => { + sinon.restore(); + }); + test('replaces backslashes with forward slashes', () => { + const testPath = 'C:\\path\\to\\file.txt'; + const result = normalizePath(testPath); + + assert.strictEqual(result.includes('\\'), false); + assert.strictEqual(result, 'C:/path/to/file.txt'); + }); + + test('converts to lowercase on Windows', () => { + isWindowsStub.returns(true); + + const testPath = 'C:/Path/To/File.txt'; + const result = normalizePath(testPath); + + assert.strictEqual(result, 'c:/path/to/file.txt'); + }); + + test('preserves case on non-Windows', () => { + isWindowsStub.returns(false); + + const testPath = 'C:/Path/To/File.txt'; + const result = normalizePath(testPath); + + assert.strictEqual(result, 'C:/Path/To/File.txt'); + }); + }); +}); diff --git a/src/test/constants.ts b/src/test/constants.ts new file mode 100644 index 0000000..4d33a6e --- /dev/null +++ b/src/test/constants.ts @@ -0,0 +1,4 @@ +import * as path from 'path'; + +export const EXTENSION_ROOT = path.dirname(path.dirname(__dirname)); +export const EXTENSION_TEST_ROOT = path.join(EXTENSION_ROOT, 'src', 'test'); diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts deleted file mode 100644 index 0418112..0000000 --- a/src/test/extension.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as assert from 'assert'; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it -import * as vscode from 'vscode'; -// import * as myExtension from '../../extension'; - -suite('Extension Test Suite', () => { - vscode.window.showInformationMessage('Start all tests.'); - - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); -}); diff --git a/src/test/features/commands/copyPathToClipboard.unit.test.ts b/src/test/features/commands/copyPathToClipboard.unit.test.ts new file mode 100644 index 0000000..19fac4a --- /dev/null +++ b/src/test/features/commands/copyPathToClipboard.unit.test.ts @@ -0,0 +1,118 @@ +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { PythonEnvironment } from '../../../api'; +import * as envApis from '../../../common/env.apis'; +import { copyPathToClipboard } from '../../../features/envCommands'; +import { + EnvManagerTreeItem, + ProjectEnvironment, + ProjectItem, + PythonEnvTreeItem, +} from '../../../features/views/treeViewItems'; +import { InternalEnvironmentManager } from '../../../internal.api'; + +suite('Copy Path To Clipboard', () => { + let clipboardWriteTextStub: sinon.SinonStub; + + setup(() => { + clipboardWriteTextStub = sinon.stub(envApis, 'clipboardWriteText'); + clipboardWriteTextStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Copy project path to clipboard', async () => { + const uri = Uri.file('/test'); + const item = new ProjectItem({ name: 'test', uri }); + await copyPathToClipboard(item); + + sinon.assert.calledOnce(clipboardWriteTextStub); + sinon.assert.calledWith(clipboardWriteTextStub, uri.fsPath); + }); + + test('Copy env path to clipboard: project view', async () => { + const uri = Uri.file('/test'); + const item = new ProjectEnvironment(new ProjectItem({ name: 'test', uri }), { + envId: { managerId: 'test-manager', id: 'env1' }, + name: 'env1', + displayName: 'Environment 1', + displayPath: '/test-env', + execInfo: { run: { executable: '/test-env/bin/test', args: ['-m', 'env'] } }, + } as PythonEnvironment); + + await copyPathToClipboard(item); + + sinon.assert.calledOnce(clipboardWriteTextStub); + sinon.assert.calledWith(clipboardWriteTextStub, '/test-env/bin/test'); + }); + + test('Copy env path to clipboard: env manager view', async () => { + const item = new PythonEnvTreeItem( + { + envId: { managerId: 'test-manager', id: 'env1' }, + name: 'env1', + displayName: 'Environment 1', + displayPath: '/test-env', + execInfo: { run: { executable: '/test-env/bin/test', args: ['-m', 'env'] } }, + } as PythonEnvironment, + new EnvManagerTreeItem({ name: 'test-manager', id: 'test-manager' } as InternalEnvironmentManager), + ); + + await copyPathToClipboard(item); + + sinon.assert.calledOnce(clipboardWriteTextStub); + sinon.assert.calledWith(clipboardWriteTextStub, '/test-env/bin/test'); + }); + + test('Copy conda env path to clipboard: should copy interpreter path not conda run command', async () => { + const item = new PythonEnvTreeItem( + { + envId: { managerId: 'conda', id: 'base' }, + name: 'base', + displayName: 'base (3.12.2)', + displayPath: '/opt/conda/envs/base', + execInfo: { + run: { executable: '/opt/conda/envs/base/bin/python' }, + activatedRun: { + executable: 'conda', + args: ['run', '--name', 'base', 'python'], + }, + }, + } as PythonEnvironment, + new EnvManagerTreeItem({ name: 'conda', id: 'conda' } as InternalEnvironmentManager), + ); + + await copyPathToClipboard(item); + + sinon.assert.calledOnce(clipboardWriteTextStub); + // Should copy the actual interpreter path, not the conda run command + sinon.assert.calledWith(clipboardWriteTextStub, '/opt/conda/envs/base/bin/python'); + }); + + test('Copy conda prefix env path to clipboard: should copy interpreter path not conda run command', async () => { + const item = new PythonEnvTreeItem( + { + envId: { managerId: 'conda', id: 'myenv' }, + name: 'myenv', + displayName: 'myenv (3.11.5)', + displayPath: '/opt/conda/envs/myenv', + execInfo: { + run: { executable: '/opt/conda/envs/myenv/bin/python' }, + activatedRun: { + executable: 'conda', + args: ['run', '--prefix', '/opt/conda/envs/myenv', 'python'], + }, + }, + } as PythonEnvironment, + new EnvManagerTreeItem({ name: 'conda', id: 'conda' } as InternalEnvironmentManager), + ); + + await copyPathToClipboard(item); + + sinon.assert.calledOnce(clipboardWriteTextStub); + // Should copy the actual interpreter path, not the conda run command + sinon.assert.calledWith(clipboardWriteTextStub, '/opt/conda/envs/myenv/bin/python'); + }); +}); diff --git a/src/test/features/common/shellDetector.unit.test.ts b/src/test/features/common/shellDetector.unit.test.ts new file mode 100644 index 0000000..16092b0 --- /dev/null +++ b/src/test/features/common/shellDetector.unit.test.ts @@ -0,0 +1,133 @@ +import assert from 'assert'; +import { Terminal } from 'vscode'; +import { isWindows } from '../../../common/utils/platformUtils'; +import { ShellConstants } from '../../../features/common/shellConstants'; +import { identifyTerminalShell } from '../../../features/common/shellDetector'; + +const testShellTypes: string[] = [ + 'sh', + 'bash', + 'powershell', + 'pwsh', + 'powershellcore', + 'cmd', + 'commandPrompt', + 'gitbash', + 'zsh', + 'ksh', + 'fish', + 'csh', + 'cshell', + 'tcsh', + 'tcshell', + 'nu', + 'nushell', + 'wsl', + 'xonsh', + 'unknown', +]; + +function getNameByShellType(shellType: string): string { + return shellType === 'unknown' ? '' : shellType; +} + +function getShellPath(shellType: string): string | undefined { + switch (shellType) { + case 'sh': + return '/bin/sh'; + case 'bash': + return '/bin/bash'; + case 'powershell': + return 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'; + case 'pwsh': + case 'powershellcore': + return 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'; + case 'cmd': + case 'commandPrompt': + return 'C:\\Windows\\System32\\cmd.exe'; + case 'gitbash': + return isWindows() ? 'C:\\Program Files\\Git\\bin\\bash.exe' : '/usr/bin/gitbash'; + case 'zsh': + return '/bin/zsh'; + case 'ksh': + return '/bin/ksh'; + case 'fish': + return '/usr/bin/fish'; + case 'csh': + case 'cshell': + return '/bin/csh'; + case 'nu': + case 'nushell': + return '/usr/bin/nu'; + case 'tcsh': + case 'tcshell': + return '/usr/bin/tcsh'; + case 'wsl': + return '/mnt/c/Windows/System32/wsl.exe'; + case 'xonsh': + return '/usr/bin/xonsh'; + default: + return undefined; + } +} + +function expectedShellType(shellType: string): string { + switch (shellType) { + case 'sh': + return ShellConstants.SH; + case 'bash': + return ShellConstants.BASH; + case 'pwsh': + case 'powershell': + case 'powershellcore': + return ShellConstants.PWSH; + case 'cmd': + case 'commandPrompt': + return ShellConstants.CMD; + case 'gitbash': + return ShellConstants.GITBASH; + case 'zsh': + return ShellConstants.ZSH; + case 'ksh': + return ShellConstants.KSH; + case 'fish': + return ShellConstants.FISH; + case 'csh': + case 'cshell': + return ShellConstants.CSH; + case 'nu': + case 'nushell': + return ShellConstants.NU; + case 'tcsh': + case 'tcshell': + return ShellConstants.TCSH; + case 'xonsh': + return ShellConstants.XONSH; + case 'wsl': + return ShellConstants.WSL; + default: + return 'unknown'; + } +} + +suite('Shell Detector', () => { + testShellTypes.forEach((shell) => { + if (shell === 'unknown') { + return; + } + + const name = getNameByShellType(shell); + test(`Detect ${shell}`, () => { + const terminal = { + name, + state: { shell }, + creationOptions: { + shellPath: getShellPath(shell), + }, + } as Terminal; + const detected = identifyTerminalShell(terminal); + const expected = expectedShellType(shell); + assert.strictEqual(detected, expected); + }); + }); +}); diff --git a/src/test/features/creators/autoFindProjects.unit.test.ts b/src/test/features/creators/autoFindProjects.unit.test.ts new file mode 100644 index 0000000..269aaa9 --- /dev/null +++ b/src/test/features/creators/autoFindProjects.unit.test.ts @@ -0,0 +1,274 @@ +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as typmoq from 'typemoq'; +import * as wapi from '../../../common/workspace.apis'; +import * as winapi from '../../../common/window.apis'; +import { PythonProjectManager } from '../../../internal.api'; +import { createDeferred } from '../../../common/utils/deferred'; +import { AutoFindProjects } from '../../../features/creators/autoFindProjects'; +import assert from 'assert'; +import { Uri } from 'vscode'; +import { PythonProject } from '../../../api'; +import { sleep } from '../../../common/utils/asyncUtils'; + +suite('Auto Find Project tests', () => { + let findFilesStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + let showQuickPickWithButtonsStub: sinon.SinonStub; + let projectManager: typmoq.IMock; + + setup(() => { + findFilesStub = sinon.stub(wapi, 'findFiles'); + showErrorMessageStub = sinon.stub(winapi, 'showErrorMessage'); + showQuickPickWithButtonsStub = sinon.stub(winapi, 'showQuickPickWithButtons'); + showQuickPickWithButtonsStub.callsFake((items) => items); + + projectManager = typmoq.Mock.ofType(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No projects found', async () => { + findFilesStub.resolves([]); + + const deferred = createDeferred(); + + let errorShown = false; + showErrorMessageStub.callsFake(() => { + errorShown = true; + deferred.resolve(); + }); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + assert.equal(result, undefined, 'Result should be undefined'); + + await Promise.race([deferred.promise, sleep(100)]); + assert.ok(errorShown, 'Error message should have been shown'); + }); + + test('No projects found (undefined)', async () => { + findFilesStub.resolves(undefined); + + const deferred = createDeferred(); + + let errorShown = false; + showErrorMessageStub.callsFake(() => { + errorShown = true; + deferred.resolve(); + }); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + assert.equal(result, undefined, 'Result should be undefined'); + + await Promise.race([deferred.promise, sleep(100)]); + assert.ok(errorShown, 'Error message should have been shown'); + }); + + test('Projects found', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + ]); + + projectManager.setup((pm) => pm.get(typmoq.It.isAny())).returns(() => undefined); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + const expected: PythonProject[] = [ + { + name: 'a', + uri: Uri.file('/usr/home/root/a'), + }, + { + name: 'b', + uri: Uri.file('/usr/home/root/b'), + }, + ]; + + assert.ok(Array.isArray(result), 'Result should be an array'); + assert.equal(result.length, expected.length, `Result should have ${expected.length} items`); + + expected.forEach((item) => { + assert.ok( + result.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in result', + ); + }); + result.forEach((item) => { + assert.ok( + expected.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in expected', + ); + }); + }); + + test('Projects found (with duplicates)', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + Uri.file('/usr/home/root/c/pyproject.toml'), + Uri.file('/usr/home/root/d/pyproject.toml'), + ]); + + projectManager + .setup((pm) => pm.get(typmoq.It.isAny())) + .returns((uri) => { + const basename = path.basename(uri.fsPath); + if (basename === 'pyproject.toml') { + const parent = path.dirname(uri.fsPath); + const name = path.basename(parent); + if (name === 'a' || name === 'd') { + return { name, uri: Uri.file(parent) }; + } + } + }); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + const expected: PythonProject[] = [ + { + name: 'b', + uri: Uri.file('/usr/home/root/b'), + }, + { + name: 'c', + uri: Uri.file('/usr/home/root/c'), + }, + ]; + + assert.ok(Array.isArray(result), 'Result should be an array'); + assert.equal(result.length, expected.length, `Result should have ${expected.length} items`); + + expected.forEach((item) => { + assert.ok( + result.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in result', + ); + }); + result.forEach((item) => { + assert.ok( + expected.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in expected', + ); + }); + }); + + test('Projects found (with all duplicates)', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + Uri.file('/usr/home/root/c/pyproject.toml'), + Uri.file('/usr/home/root/d/pyproject.toml'), + ]); + + projectManager + .setup((pm) => pm.get(typmoq.It.isAny())) + .returns((uri) => { + const basename = path.basename(uri.fsPath); + if (basename === 'pyproject.toml') { + const parent = path.dirname(uri.fsPath); + const name = path.basename(parent); + return { name, uri: Uri.file(parent) }; + } + }); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + assert.equal(result, undefined, 'Result should be undefined'); + }); + + test('Projects found no selection', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + ]); + + projectManager.setup((pm) => pm.get(typmoq.It.isAny())).returns(() => undefined); + + showQuickPickWithButtonsStub.callsFake(() => []); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + assert.equal(result, undefined, 'Result should be undefined'); + }); + + test('Projects found with no selection (user hit escape in picker)', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + ]); + + projectManager.setup((pm) => pm.get(typmoq.It.isAny())).returns(() => undefined); + + showQuickPickWithButtonsStub.callsFake(() => undefined); + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + assert.equal(result, undefined, 'Result should be undefined'); + }); + + test('Projects found with selection', async () => { + findFilesStub.resolves([ + Uri.file('/usr/home/root/a/pyproject.toml'), + Uri.file('/usr/home/root/b/pyproject.toml'), + Uri.file('/usr/home/root/c/pyproject.toml'), + Uri.file('/usr/home/root/d/pyproject.toml'), + ]); + + projectManager + .setup((pm) => pm.get(typmoq.It.isAny())) + .returns((uri) => { + const basename = path.basename(uri.fsPath); + if (basename === 'pyproject.toml') { + const parent = path.dirname(uri.fsPath); + const name = path.basename(parent); + if (name === 'c') { + return { name, uri: Uri.file(parent) }; + } + } + }); + + showQuickPickWithButtonsStub.callsFake((items) => { + return [items[0], items[2]]; + }); + + const expected: PythonProject[] = [ + { + name: 'a', + uri: Uri.file('/usr/home/root/a'), + }, + { + name: 'd', + uri: Uri.file('/usr/home/root/d'), + }, + ]; + + const autoFindProjects = new AutoFindProjects(projectManager.object); + const result = await autoFindProjects.create(); + + assert.ok(Array.isArray(result), 'Result should be an array'); + assert.equal(result.length, expected.length, `Result should have ${expected.length} items`); + + expected.forEach((item) => { + assert.ok( + result.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in result', + ); + }); + result.forEach((item) => { + assert.ok( + expected.some((r) => r.name === item.name && r.uri.fsPath === item.uri.fsPath), + 'Item not found in expected', + ); + }); + }); +}); diff --git a/src/test/features/envCommands.unit.test.ts b/src/test/features/envCommands.unit.test.ts new file mode 100644 index 0000000..311e67d --- /dev/null +++ b/src/test/features/envCommands.unit.test.ts @@ -0,0 +1,177 @@ +import * as assert from 'assert'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { EnvironmentManagers, InternalEnvironmentManager, PythonProjectManager } from '../../internal.api'; +import * as projectApi from '../../common/pickers/projects'; +import * as managerApi from '../../common/pickers/managers'; +import { PythonEnvironment, PythonProject } from '../../api'; +import { createAnyEnvironmentCommand } from '../../features/envCommands'; +import { Uri } from 'vscode'; + +suite('Create Any Environment Command Tests', () => { + let em: typeMoq.IMock; + let pm: typeMoq.IMock; + let manager: typeMoq.IMock; + let env: typeMoq.IMock; + let pickProjectManyStub: sinon.SinonStub; + let pickEnvironmentManagerStub: sinon.SinonStub; + let project: PythonProject = { + uri: Uri.file('/some/test/workspace/folder'), + name: 'test-folder', + }; + let project2: PythonProject = { + uri: Uri.file('/some/test/workspace/folder2'), + name: 'test-folder2', + }; + let project3: PythonProject = { + uri: Uri.file('/some/test/workspace/folder3'), + name: 'test-folder3', + }; + + setup(() => { + manager = typeMoq.Mock.ofType(); + manager.setup((m) => m.id).returns(() => 'test'); + manager.setup((m) => m.displayName).returns(() => 'Test Manager'); + manager.setup((m) => m.description).returns(() => 'Test Manager Description'); + manager.setup((m) => m.supportsCreate).returns(() => true); + + env = typeMoq.Mock.ofType(); + env.setup((e) => e.envId).returns(() => ({ id: 'env1', managerId: 'test' })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + env.setup((e: any) => e.then).returns(() => undefined); + + em = typeMoq.Mock.ofType(); + em.setup((e) => e.managers).returns(() => [manager.object]); + em.setup((e) => e.getEnvironmentManager(typeMoq.It.isAnyString())).returns(() => manager.object); + + pm = typeMoq.Mock.ofType(); + + pickEnvironmentManagerStub = sinon.stub(managerApi, 'pickEnvironmentManager'); + pickProjectManyStub = sinon.stub(projectApi, 'pickProjectMany'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Create global venv (no-workspace): no-select', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => []); + manager + .setup((m) => m.create('global', typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + manager.setup((m) => m.set(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: false }); + // Add assertions to verify the result + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + }); + + test('Create global venv (no-workspace): select', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => []); + manager + .setup((m) => m.create('global', typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + manager.setup((m) => m.set(undefined, env.object)).verifiable(typeMoq.Times.once()); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: true }); + // Add assertions to verify the result + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + }); + + test('Create workspace venv: no-select', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => [project]); + manager + .setup((m) => m.create([project.uri], typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + manager.setup((m) => m.set(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + em.setup((e) => e.setEnvironments(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([project]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: false }); + + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + em.verifyAll(); + }); + + test('Create workspace venv: select', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => [project]); + manager + .setup((m) => m.create([project.uri], typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + // This is a case where env managers handler does this in batch to avoid writing to files for each case + manager.setup((m) => m.set(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + em.setup((e) => e.setEnvironments([project.uri], env.object)).verifiable(typeMoq.Times.once()); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([project]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: true }); + + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + em.verifyAll(); + }); + + test('Create multi-workspace venv: select all', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => [project, project2, project3]); + manager + .setup((m) => m.create([project.uri, project2.uri, project3.uri], typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + // This is a case where env managers handler does this in batch to avoid writing to files for each case + manager.setup((m) => m.set(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + em.setup((e) => e.setEnvironments([project.uri, project2.uri, project3.uri], env.object)).verifiable( + typeMoq.Times.once(), + ); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([project, project2, project3]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: true }); + + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + em.verifyAll(); + }); + + test('Create multi-workspace venv: select some', async () => { + pm.setup((p) => p.getProjects(typeMoq.It.isAny())).returns(() => [project, project2, project3]); + manager + .setup((m) => m.create([project.uri, project3.uri], typeMoq.It.isAny())) + .returns(() => Promise.resolve(env.object)) + .verifiable(typeMoq.Times.once()); + + // This is a case where env managers handler does this in batch to avoid writing to files for each case + manager.setup((m) => m.set(typeMoq.It.isAny(), typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); + em.setup((e) => e.setEnvironments([project.uri, project3.uri], env.object)).verifiable(typeMoq.Times.once()); + + pickEnvironmentManagerStub.resolves(manager.object.id); + pickProjectManyStub.resolves([project, project3]); + + const result = await createAnyEnvironmentCommand(em.object, pm.object, { selectEnvironment: true }); + + assert.strictEqual(result, env.object, 'Expected the created environment to match the mocked environment.'); + manager.verifyAll(); + em.verifyAll(); + }); +}); diff --git a/src/test/features/execution/execUtils.unit.test.ts b/src/test/features/execution/execUtils.unit.test.ts new file mode 100644 index 0000000..82dd808 --- /dev/null +++ b/src/test/features/execution/execUtils.unit.test.ts @@ -0,0 +1,158 @@ +import * as assert from 'assert'; +import { quoteArgs, quoteStringIfNecessary } from '../../../features/execution/execUtils'; + +suite('Execution Utils Tests', () => { + suite('quoteStringIfNecessary', () => { + test('should not quote string without spaces', () => { + const input = 'simplestring'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, 'simplestring'); + }); + + test('should not quote string without spaces containing special characters', () => { + const input = 'path/to/file.txt'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, 'path/to/file.txt'); + }); + + test('should quote string with spaces', () => { + const input = 'string with spaces'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"string with spaces"'); + }); + + test('should quote path with spaces', () => { + const input = 'C:\\Program Files\\Python'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"C:\\Program Files\\Python"'); + }); + + test('should not double-quote already quoted string', () => { + const input = '"already quoted"'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"already quoted"'); + }); + + test('should not double-quote already quoted string with spaces', () => { + const input = '"string with spaces"'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"string with spaces"'); + }); + + test('should quote string with space that is partially quoted', () => { + const input = '"partially quoted'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '""partially quoted"'); + }); + + test('should quote string with space that ends with quote', () => { + const input = 'partially quoted"'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"partially quoted""'); + }); + + test('should handle empty string', () => { + const input = ''; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, ''); + }); + + test('should handle string with only spaces', () => { + const input = ' '; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '" "'); + }); + + test('should handle string with leading space', () => { + const input = ' leading'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '" leading"'); + }); + + test('should handle string with trailing space', () => { + const input = 'trailing '; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"trailing "'); + }); + + test('should handle string with multiple spaces', () => { + const input = 'multiple spaces here'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '"multiple spaces here"'); + }); + + test('should not quote single character without space', () => { + const input = 'a'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, 'a'); + }); + + test('should handle dash and hyphen characters without spaces', () => { + const input = '--flag-name'; + const result = quoteStringIfNecessary(input); + assert.strictEqual(result, '--flag-name'); + }); + }); + + suite('quoteArgs', () => { + test('should return empty array for empty input', () => { + const input: string[] = []; + const result = quoteArgs(input); + assert.deepStrictEqual(result, []); + }); + + test('should not quote args without spaces', () => { + const input = ['arg1', 'arg2', 'arg3']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['arg1', 'arg2', 'arg3']); + }); + + test('should quote args with spaces', () => { + const input = ['arg with spaces', 'another arg']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['"arg with spaces"', '"another arg"']); + }); + + test('should handle mixed args with and without spaces', () => { + const input = ['simplearg', 'arg with spaces', 'anotherarg']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['simplearg', '"arg with spaces"', 'anotherarg']); + }); + + test('should not double-quote already quoted args', () => { + const input = ['"already quoted"', 'normal']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['"already quoted"', 'normal']); + }); + + test('should handle array with single element', () => { + const input = ['single element with space']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['"single element with space"']); + }); + + test('should handle paths correctly', () => { + const input = ['C:\\Program Files\\Python', '/usr/bin/python', 'simple']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['"C:\\Program Files\\Python"', '/usr/bin/python', 'simple']); + }); + + test('should handle command line flags and values', () => { + const input = ['--flag', 'value with spaces', '-f', 'normalvalue']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['--flag', '"value with spaces"', '-f', 'normalvalue']); + }); + + test('should handle empty strings in array', () => { + const input = ['', 'arg1', '']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['', 'arg1', '']); + }); + + test('should preserve order of arguments', () => { + const input = ['first', 'second with space', 'third', 'fourth with space']; + const result = quoteArgs(input); + assert.deepStrictEqual(result, ['first', '"second with space"', 'third', '"fourth with space"']); + }); + }); +}); diff --git a/src/test/features/execution/runAsTask.unit.test.ts b/src/test/features/execution/runAsTask.unit.test.ts new file mode 100644 index 0000000..dbb5816 --- /dev/null +++ b/src/test/features/execution/runAsTask.unit.test.ts @@ -0,0 +1,749 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Task, TaskExecution, TaskPanelKind, TaskRevealKind, TaskScope, Uri, WorkspaceFolder } from 'vscode'; +import { PythonEnvironment, PythonTaskExecutionOptions } from '../../../api'; +import * as logging from '../../../common/logging'; +import * as tasksApi from '../../../common/tasks.apis'; +import * as workspaceApis from '../../../common/workspace.apis'; +import * as execUtils from '../../../features/execution/execUtils'; +import { runAsTask } from '../../../features/execution/runAsTask'; + +suite('runAsTask Tests', () => { + let mockTraceInfo: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + let mockExecuteTask: sinon.SinonStub; + let mockGetWorkspaceFolder: sinon.SinonStub; + let mockQuoteStringIfNecessary: sinon.SinonStub; + + setup(() => { + mockTraceInfo = sinon.stub(logging, 'traceInfo'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + mockExecuteTask = sinon.stub(tasksApi, 'executeTask'); + mockGetWorkspaceFolder = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + mockQuoteStringIfNecessary = sinon.stub(execUtils, 'quoteStringIfNecessary'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Happy Path Scenarios', () => { + test('should create and execute task with activated run configuration', async () => { + // Mock - Environment with activatedRun + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + shortDisplayName: 'TestEnv', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + args: ['--default'], + }, + activatedRun: { + executable: '/activated/python', + args: ['--activated'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Test Task', + args: ['script.py', '--arg1'], + project: { + name: 'Test Project', + uri: Uri.file('/workspace'), + }, + cwd: '/workspace', + env: { PATH: '/custom/path' }, + }; + + const mockWorkspaceFolder: WorkspaceFolder = { + uri: Uri.file('/workspace'), + name: 'Test Workspace', + index: 0, + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.withArgs(options.project?.uri).returns(mockWorkspaceFolder); + mockQuoteStringIfNecessary.withArgs('/activated/python').returns('"/activated/python"'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify task creation + assert.ok(mockExecuteTask.calledOnce, 'Should execute task once'); + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + + assert.strictEqual(taskArg.definition.type, 'python', 'Task type should be python'); + assert.strictEqual(taskArg.scope, mockWorkspaceFolder, 'Task scope should be workspace folder'); + assert.strictEqual(taskArg.name, 'Test Task', 'Task name should match options'); + assert.strictEqual(taskArg.source, 'Python', 'Task source should be Python'); + assert.deepStrictEqual(taskArg.problemMatchers, ['$python'], 'Should use python problem matcher'); + + // Verify presentation options + assert.strictEqual( + taskArg.presentationOptions?.reveal, + TaskRevealKind.Silent, + 'Should use silent reveal by default', + ); + assert.strictEqual(taskArg.presentationOptions?.echo, true, 'Should echo commands'); + assert.strictEqual(taskArg.presentationOptions?.panel, TaskPanelKind.Shared, 'Should use shared panel'); + assert.strictEqual(taskArg.presentationOptions?.close, false, 'Should not close panel'); + assert.strictEqual(taskArg.presentationOptions?.showReuseMessage, true, 'Should show reuse message'); + + // Verify logging + assert.ok( + mockTraceInfo.calledWith( + sinon.match(/Running as task: "\/activated\/python" --activated script\.py --arg1/), + ), + 'Should log execution command', + ); + + // Verify no warnings + assert.ok(mockTraceWarn.notCalled, 'Should not log warnings for valid environment'); + }); + + test('should create and execute task with regular run configuration when no activatedRun', async () => { + // Mock - Environment without activatedRun + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + args: ['--default-arg'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Simple Task', + args: ['test.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.withArgs(undefined).returns(undefined); + mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + assert.strictEqual(taskArg.scope, TaskScope.Global, 'Should use global scope when no workspace'); + + // Verify logging shows correct executable and args + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: \/path\/to\/python --default-arg test\.py/)), + 'Should log execution with run args', + ); + }); + + test('should handle custom reveal option', async () => { + // Mock - Test custom reveal option + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Reveal Task', + args: ['script.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run with custom reveal option + await runAsTask(environment, options, { reveal: TaskRevealKind.Always }); + + // Assert + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + assert.strictEqual( + taskArg.presentationOptions?.reveal, + TaskRevealKind.Always, + 'Should use custom reveal option', + ); + }); + }); + + suite('Edge Cases', () => { + test('should handle environment without execInfo', async () => { + // Mock - Environment with no execInfo + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + sysPrefix: '/path/to/env', + } as PythonEnvironment; + + const options: PythonTaskExecutionOptions = { + name: 'No ExecInfo Task', + args: ['script.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify fallback to 'python' and warning + assert.ok( + mockTraceWarn.calledWith('No Python executable found in environment; falling back to "python".'), + 'Should warn about missing executable', + ); + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: python script\.py/)), + 'Should log with fallback executable', + ); + }); + + test('should handle environment with empty execInfo run args', async () => { + // Mock - Environment with empty args + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + // No args provided + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Empty Args Task', + args: ['script.py', '--verbose'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('/path/to/python').returns('/path/to/python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify only option args are used + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: \/path\/to\/python script\.py --verbose/)), + 'Should log with only option args', + ); + }); + + test('should handle options with no args', async () => { + // Mock - Options with empty args + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: ['--version-check'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'No Args Task', + args: [], // Empty args + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify only environment args are used + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: python --version-check/)), + 'Should log with only environment args', + ); + }); + + test('should handle executable paths with spaces requiring quoting', async () => { + // Mock - Executable path with spaces + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path with spaces/to/python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Spaced Path Task', + args: ['script.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('/path with spaces/to/python').returns('"/path with spaces/to/python"'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify quoting function is called + assert.ok( + mockQuoteStringIfNecessary.calledWith('/path with spaces/to/python'), + 'Should call quoting function for executable', + ); + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: "\/path with spaces\/to\/python" script\.py/)), + 'Should log with quoted executable', + ); + }); + }); + + suite('Workspace Resolution', () => { + test('should use workspace folder when project URI is provided', async () => { + // Mock - Test workspace resolution + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const projectUri = Uri.file('/workspace/project'); + const options: PythonTaskExecutionOptions = { + name: 'Workspace Task', + args: ['script.py'], + project: { + name: 'Test Project', + uri: projectUri, + }, + }; + + const mockWorkspaceFolder: WorkspaceFolder = { + uri: Uri.file('/workspace'), + name: 'Workspace', + index: 0, + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.withArgs(projectUri).returns(mockWorkspaceFolder); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + assert.strictEqual(taskArg.scope, mockWorkspaceFolder, 'Should use resolved workspace folder as scope'); + + // Verify workspace lookup was called correctly + assert.ok( + mockGetWorkspaceFolder.calledWith(projectUri), + 'Should look up workspace folder with project URI', + ); + }); + + test('should use global scope when no workspace folder found', async () => { + // Mock - No workspace folder found + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Global Task', + args: ['script.py'], + project: { + name: 'Test Project', + uri: Uri.file('/non-workspace/project'), + }, + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + assert.strictEqual( + taskArg.scope, + TaskScope.Global, + 'Should fallback to global scope when workspace not found', + ); + }); + }); + + suite('Task Configuration', () => { + test('should correctly combine environment and option args', async () => { + // Mock - Test arg combination + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + activatedRun: { + executable: 'python', + args: ['--env-arg1', '--env-arg2'], + }, + run: { + executable: 'fallback-python', + args: ['--fallback'], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Combined Args Task', + args: ['--opt-arg1', 'script.py', '--opt-arg2'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + // Verify args are combined correctly (environment args first, then option args) + assert.ok( + mockTraceInfo.calledWith( + sinon.match(/Running as task: python --env-arg1 --env-arg2 --opt-arg1 script\.py --opt-arg2/), + ), + 'Should log with combined args in correct order', + ); + }); + + test('should pass through cwd and env options to shell execution', async () => { + // Mock - Test shell execution options + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Shell Options Task', + args: ['script.py'], + cwd: '/custom/working/dir', + env: { + CUSTOM_VAR: 'custom_value', + PATH: '/custom/path', + }, + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should return the task execution result'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + + // Verify shell execution was created with correct options + // Note: We can't easily inspect ShellExecution internals, but we can verify the task was created + assert.ok(taskArg.execution, 'Task should have execution configured'); + assert.strictEqual(taskArg.name, 'Shell Options Task', 'Task should have correct name'); + }); + }); + + suite('Error Scenarios', () => { + test('should propagate task execution failures', async () => { + // Mock - Task execution failure + const environment: PythonEnvironment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'Test Environment', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + execInfo: { + run: { + executable: 'python', + args: [], + }, + }, + sysPrefix: '/path/to/env', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Failing Task', + args: ['script.py'], + }; + + const executionError = new Error('Task execution failed'); + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.rejects(executionError); + + // Run & Assert + await assert.rejects( + () => runAsTask(environment, options), + executionError, + 'Should propagate task execution error', + ); + + // Verify logging still occurred before failure + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: python script\.py/)), + 'Should log before execution attempt', + ); + }); + }); + + suite('Integration Scenarios', () => { + test('should work with minimal environment and options', async () => { + // Mock - Minimal valid configuration + const environment: PythonEnvironment = { + envId: { id: 'minimal-env', managerId: 'minimal-manager' }, + name: 'Minimal Environment', + displayName: 'Minimal Environment', + displayPath: '/minimal/env', + version: '3.8.0', + environmentPath: Uri.file('/minimal/env'), + sysPrefix: '/minimal/env', + // No execInfo - should fallback to 'python' + } as PythonEnvironment; + + const options: PythonTaskExecutionOptions = { + name: 'Minimal Task', + args: ['hello.py'], + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.returns(undefined); + mockQuoteStringIfNecessary.withArgs('python').returns('python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should successfully execute with minimal configuration'); + assert.ok(mockTraceWarn.calledOnce, 'Should warn about missing executable'); + assert.ok( + mockTraceInfo.calledWith(sinon.match(/Running as task: python hello\.py/)), + 'Should log with fallback executable', + ); + }); + + test('should handle complex real-world scenario', async () => { + // Mock - Complex real-world environment + const environment: PythonEnvironment = { + envId: { id: 'venv-1', managerId: 'virtualenv' }, + name: 'Project Virtual Environment', + displayName: 'myproject-venv (Python 3.11.0)', + shortDisplayName: 'myproject-venv', + displayPath: '~/projects/myproject/.venv', + version: '3.11.0', + environmentPath: Uri.file('/Users/user/projects/myproject/.venv'), + description: 'Virtual environment for myproject', + execInfo: { + run: { + executable: '/Users/user/projects/myproject/.venv/bin/python', + args: [], + }, + activatedRun: { + executable: '/Users/user/projects/myproject/.venv/bin/python', + args: ['-m', 'site'], + }, + activation: [ + { + executable: 'source', + args: ['/Users/user/projects/myproject/.venv/bin/activate'], + }, + ], + }, + sysPrefix: '/Users/user/projects/myproject/.venv', + group: 'Virtual Environments', + }; + + const options: PythonTaskExecutionOptions = { + name: 'Run Tests', + args: ['-m', 'pytest', 'tests/', '-v', '--tb=short'], + project: { + name: 'MyProject', + uri: Uri.file('/Users/user/projects/myproject'), + description: 'My Python Project', + }, + cwd: '/Users/user/projects/myproject', + env: { + PYTHONPATH: '/Users/user/projects/myproject/src', + TEST_ENV: 'development', + }, + }; + + const mockWorkspaceFolder: WorkspaceFolder = { + uri: Uri.file('/Users/user/projects'), + name: 'Projects', + index: 0, + }; + + const mockTaskExecution = {} as TaskExecution; + + mockGetWorkspaceFolder.withArgs(options.project?.uri).returns(mockWorkspaceFolder); + mockQuoteStringIfNecessary + .withArgs('/Users/user/projects/myproject/.venv/bin/python') + .returns('/Users/user/projects/myproject/.venv/bin/python'); + mockExecuteTask.resolves(mockTaskExecution); + + // Run + const result = await runAsTask(environment, options, { reveal: TaskRevealKind.Always }); + + // Assert + assert.strictEqual(result, mockTaskExecution, 'Should handle complex real-world scenario'); + + const taskArg = mockExecuteTask.firstCall.args[0] as Task; + assert.strictEqual(taskArg.name, 'Run Tests', 'Should use correct task name'); + assert.strictEqual(taskArg.scope, mockWorkspaceFolder, 'Should use correct workspace scope'); + assert.strictEqual( + taskArg.presentationOptions?.reveal, + TaskRevealKind.Always, + 'Should use custom reveal setting', + ); + + // Verify complex args are logged correctly + assert.ok( + mockTraceInfo.calledWith( + sinon.match( + /Running as task: \/Users\/user\/projects\/myproject\/\.venv\/bin\/python -m site -m pytest tests\/ -v --tb=short/, + ), + ), + 'Should log complex command with all args', + ); + + // Verify no warnings for complete environment + assert.ok(mockTraceWarn.notCalled, 'Should not warn for complete environment configuration'); + }); + }); +}); diff --git a/src/test/features/execution/runInBackground.unit.test.ts b/src/test/features/execution/runInBackground.unit.test.ts new file mode 100644 index 0000000..7a0b88e --- /dev/null +++ b/src/test/features/execution/runInBackground.unit.test.ts @@ -0,0 +1,421 @@ +import * as cp from 'child_process'; +import assert from 'node:assert'; +import path from 'node:path'; +import * as sinon from 'sinon'; + +import { Uri } from 'vscode'; +import { PythonBackgroundRunOptions, PythonEnvironment } from '../../../api'; +import * as childProcessApis from '../../../common/childProcess.apis'; +import * as logging from '../../../common/logging'; +import * as execUtils from '../../../features/execution/execUtils'; +import { runInBackground } from '../../../features/execution/runInBackground'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; + +/** + * Creates a mock PythonEnvironment for testing purposes. + * + * This helper function generates a complete PythonEnvironment object with sensible defaults + * while allowing customization of the execInfo property, which is crucial for testing + * different execution scenarios (activated vs non-activated environments, missing configs, etc.). + * + * @param execInfo - Execution information object containing run/activatedRun configs. + * Pass null to create an environment without execInfo (tests fallback behavior). + * Pass undefined or omit to get default execInfo with basic python3 executable. + * Pass custom object to test specific execution configurations. + * @returns A complete PythonEnvironment object suitable for testing + * + * @example + * // Environment with default execInfo + * const env = createMockEnvironment(); + * + * // Environment without execInfo (tests fallback to 'python') + * const envNoExec = createMockEnvironment(null); + * + * // Environment with custom execution config + * const envCustom = createMockEnvironment({ + * run: { executable: '/custom/python', args: ['--flag'] }, + * activatedRun: { executable: '/venv/python', args: [] } + * }); + */ +function createMockEnvironment(execInfo?: object | null): PythonEnvironment { + const baseEnv = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'test-env', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + sysPrefix: '/path/to/sys/prefix', + }; + + if (execInfo === null) { + // Return environment without execInfo for testing fallback scenarios + return baseEnv as PythonEnvironment; + } + + return { + ...baseEnv, + execInfo: (execInfo || { + run: { executable: path.join('usr', 'bin', 'python3'), args: [] }, + }) as PythonEnvironment['execInfo'], + }; +} + +// Store the created mock processes for testing +let _mockProcesses: MockChildProcess[] = []; + +// Track spawned processes for testing +interface SpawnCall { + executable: string; + args: string[]; + options: cp.SpawnOptions; +} + +let _spawnCalls: SpawnCall[] = []; + +suite('runInBackground Function Tests', () => { + let mockTraceInfo: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockQuoteStringIfNecessary: sinon.SinonStub; + let mockExistsSync: sinon.SinonStub; + let mockSpawnProcess: sinon.SinonStub; + + setup(() => { + // Reset tracking arrays + _spawnCalls = []; + _mockProcesses = []; + + // Mock logging functions + mockTraceInfo = sinon.stub(logging, 'traceInfo'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + mockTraceError = sinon.stub(logging, 'traceError'); + + // Mock execUtils + mockQuoteStringIfNecessary = sinon.stub(execUtils, 'quoteStringIfNecessary'); + mockQuoteStringIfNecessary.callsFake((arg: string) => arg); + + // Mock fs.existsSync to avoid file system checks + mockExistsSync = sinon.stub(); + mockExistsSync.returns(true); + const fs = require('fs'); + sinon.stub(fs, 'existsSync').callsFake(mockExistsSync); + + // Mock spawnProcess to capture calls and return mock process + mockSpawnProcess = sinon.stub(childProcessApis, 'spawnProcess'); + mockSpawnProcess.callsFake((command: string, args: string[], options?: cp.SpawnOptions) => { + // Track the spawn call for assertions + _spawnCalls.push({ + executable: command, + args: args, + options: options || {}, + }); + + // Create and return a mock child process that won't actually spawn + const mockProcess = new MockChildProcess(command, args); + _mockProcesses.push(mockProcess); + + // Set the pid property directly on the object + Object.defineProperty(mockProcess, 'pid', { + value: 12345, + writable: false, + configurable: true, + }); + + // Return the mock process (it extends EventEmitter and has stdin/stdout/stderr) + return mockProcess as unknown as cp.ChildProcess; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Executable and Args Logic', () => { + test('should prefer activatedRun executable over run executable', async () => { + // Mock → Environment with both activatedRun and run executables + const environment = createMockEnvironment({ + run: { + executable: path.join('usr', 'bin', 'python3'), + args: ['--base-arg'], + }, + activatedRun: { + executable: path.join('path', 'to', 'venv', 'python'), + args: ['--activated-arg'], + }, + }); + + const options: PythonBackgroundRunOptions = { + args: ['script.py', '--script-arg'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual( + spawnCall.executable, + path.join('path', 'to', 'venv', 'python'), + 'Should prefer activatedRun executable', + ); + assert.deepStrictEqual( + spawnCall.args, + ['--activated-arg', 'script.py', '--script-arg'], + 'Should combine activatedRun args with options args', + ); + }); + + test('should fallback to run executable when activatedRun not available', async () => { + // Mock → Environment with only run executable + const environment = createMockEnvironment({ + run: { + executable: path.join('usr', 'bin', 'python3'), + args: ['--base-arg'], + }, + }); + + const options: PythonBackgroundRunOptions = { + args: ['module', '-m', 'pip', 'list'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual(spawnCall.executable, path.join('usr', 'bin', 'python3'), 'Should use run executable'); + assert.deepStrictEqual( + spawnCall.args, + ['--base-arg', 'module', '-m', 'pip', 'list'], + 'Should combine run args with options args', + ); + }); + + test('should fallback to "python" when no executable found', async () => { + // Mock → Environment with no execInfo + const environment = createMockEnvironment(null); + + const options: PythonBackgroundRunOptions = { + args: ['script.py'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual(spawnCall.executable, 'python', 'Should fallback to "python"'); + assert.deepStrictEqual(spawnCall.args, ['script.py'], 'Should use options args only'); + }); + + test('should remove quotes from executable path', async () => { + // Mock → Environment with quoted executable path + const environment = createMockEnvironment({ + run: { + executable: `"${path.join('path with spaces', 'python')}"`, + args: [], + }, + }); + + const options: PythonBackgroundRunOptions = { + args: ['script.py'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual( + spawnCall.executable, + path.join('path with spaces', 'python'), + 'Should remove surrounding quotes', + ); + }); + + test('should handle empty args arrays', async () => { + // Mock → Environment with no args and options with no args + const environment = createMockEnvironment({ + run: { + executable: path.join('usr', 'bin', 'python3'), + // No args property + }, + }); + + const options: PythonBackgroundRunOptions = { + args: [], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.deepStrictEqual(spawnCall.args, [], 'Should handle empty args arrays'); + }); + + test('should combine environment args with options args correctly', async () => { + // Mock → Complex environment with all options + const environment = createMockEnvironment({ + run: { + executable: path.join('usr', 'bin', 'python3'), + args: ['--base'], + }, + activatedRun: { + executable: path.join('venv', 'bin', 'python'), + args: ['--activated', '--optimized'], + }, + }); + + const options: PythonBackgroundRunOptions = { + args: ['-m', 'mymodule', '--config', 'production'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual(spawnCall.executable, path.join('venv', 'bin', 'python'), 'Should prefer activatedRun'); + + const expectedArgs = ['--activated', '--optimized', '-m', 'mymodule', '--config', 'production']; + assert.deepStrictEqual(spawnCall.args, expectedArgs, 'Should combine all args correctly'); + }); + }); + + suite('Logging Behavior', () => { + test('should have proper logging methods available', () => { + // Assert - verify logging functions exist + assert.ok(mockTraceInfo, 'Should have traceInfo mock'); + assert.ok(mockTraceWarn, 'Should have traceWarn mock'); + assert.ok(mockTraceError, 'Should have traceError mock'); + }); + + test('should have execUtils methods available', () => { + // Assert - verify execUtils functions exist + assert.ok(mockQuoteStringIfNecessary, 'Should have quoteStringIfNecessary mock'); + + // Test the default behavior + const result = mockQuoteStringIfNecessary('test-path'); + assert.strictEqual(result, 'test-path', 'Should return path unchanged by default'); + }); + }); + + suite('Environment Structure Tests', () => { + test('should create valid PythonEnvironment mock', () => { + // Mock → Complete environment + const environment = createMockEnvironment({ + run: { executable: path.join('usr', 'bin', 'python3'), args: ['--arg'] }, + activatedRun: { executable: path.join('venv', 'python'), args: ['--venv-arg'] }, + }); + + // Assert - verify structure + assert.ok(environment.envId, 'Should have envId'); + assert.strictEqual(environment.envId.id, 'test-env', 'Should have correct id'); + assert.strictEqual(environment.envId.managerId, 'test-manager', 'Should have correct managerId'); + assert.strictEqual(environment.name, 'test-env', 'Should have correct name'); + assert.strictEqual(environment.displayName, 'Test Environment', 'Should have correct displayName'); + assert.ok(environment.execInfo, 'Should have execInfo'); + assert.ok(environment.execInfo.run, 'Should have run config'); + assert.ok(environment.execInfo.activatedRun, 'Should have activatedRun config'); + }); + + test('should create PythonBackgroundRunOptions correctly', () => { + // Mock → Options with all properties + const options: PythonBackgroundRunOptions = { + args: ['-m', 'pytest', 'tests/', '--verbose'], + cwd: '/project/root', + env: { PYTHONPATH: '/custom/path', DEBUG: 'true' }, + }; + + // Assert - verify structure + assert.ok(Array.isArray(options.args), 'Should have args array'); + assert.strictEqual(options.args.length, 4, 'Should have correct number of args'); + assert.strictEqual(options.cwd, '/project/root', 'Should have correct cwd'); + assert.ok(options.env, 'Should have env object'); + assert.strictEqual(options.env.PYTHONPATH, '/custom/path', 'Should have correct PYTHONPATH'); + assert.strictEqual(options.env.DEBUG, 'true', 'Should have correct DEBUG value'); + }); + }); + + suite('Edge Cases and Error Conditions', () => { + test('should handle missing execInfo gracefully', async () => { + // Mock → Environment without execInfo + const environment = { + envId: { id: 'test-env', managerId: 'test-manager' }, + name: 'test-env', + displayName: 'Test Environment', + displayPath: '/path/to/env', + version: '3.9.0', + environmentPath: Uri.file('/path/to/env'), + sysPrefix: '/path/to/sys/prefix', + // No execInfo + } as PythonEnvironment; + + const options: PythonBackgroundRunOptions = { + args: ['script.py'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual(spawnCall.executable, 'python', 'Should fallback to "python"'); + assert.deepStrictEqual(spawnCall.args, ['script.py'], 'Should use options args only'); + }); + + test('should handle partial execInfo configurations', async () => { + // Mock → Environment with run but no activatedRun + const environment = createMockEnvironment({ + run: { + executable: path.join('usr', 'bin', 'python3'), + // No args + }, + // No activatedRun + }); + + const options: PythonBackgroundRunOptions = { + args: ['--help'], + }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual(spawnCall.executable, path.join('usr', 'bin', 'python3'), 'Should use run executable'); + assert.deepStrictEqual(spawnCall.args, ['--help'], 'Should handle missing environment args'); + }); + + test('should handle quote patterns correctly', async () => { + // Test the common case of quoted paths with spaces + const environment = createMockEnvironment({ + run: { executable: `"${path.join('path with spaces', 'python')}"`, args: [] }, + }); + + const options: PythonBackgroundRunOptions = { args: [] }; + + // Run + await runInBackground(environment, options); + + // Assert + assert.strictEqual(_spawnCalls.length, 1, 'Should call spawn once'); + const spawnCall = _spawnCalls[0]; + assert.strictEqual( + spawnCall.executable, + path.join('path with spaces', 'python'), + 'Should remove surrounding quotes from executable path', + ); + }); + }); +}); diff --git a/src/test/features/reportIssue.unit.test.ts b/src/test/features/reportIssue.unit.test.ts new file mode 100644 index 0000000..8f80c70 --- /dev/null +++ b/src/test/features/reportIssue.unit.test.ts @@ -0,0 +1,112 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as assert from 'assert'; +import * as typeMoq from 'typemoq'; +import * as vscode from 'vscode'; +import { PythonEnvironment, PythonEnvironmentId } from '../../api'; +import { EnvironmentManagers, PythonProjectManager } from '../../internal.api'; +import { PythonProject } from '../../api'; + +// We need to mock the extension's activate function to test the collectEnvironmentInfo function +// Since it's a local function, we'll test the command registration instead + +suite('Report Issue Command Tests', () => { + let mockEnvManagers: typeMoq.IMock; + let mockProjectManager: typeMoq.IMock; + + setup(() => { + mockEnvManagers = typeMoq.Mock.ofType(); + mockProjectManager = typeMoq.Mock.ofType(); + }); + + test('should handle environment collection with empty data', () => { + mockEnvManagers.setup((em) => em.managers).returns(() => []); + mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []); + + // Test that empty collections are handled gracefully + const managers = mockEnvManagers.object.managers; + const projects = mockProjectManager.object.getProjects(); + + assert.strictEqual(managers.length, 0); + assert.strictEqual(projects.length, 0); + }); + + test('should handle environment collection with mock data', async () => { + // Create mock environment + const mockEnvId: PythonEnvironmentId = { + id: 'test-env-id', + managerId: 'test-manager' + }; + + const mockEnv: PythonEnvironment = { + envId: mockEnvId, + name: 'Test Environment', + displayName: 'Test Environment 3.9', + displayPath: '/path/to/python', + version: '3.9.0', + environmentPath: vscode.Uri.file('/path/to/env'), + execInfo: { + run: { + executable: '/path/to/python', + args: [] + } + }, + sysPrefix: '/path/to/env' + }; + + const mockManager = { + id: 'test-manager', + displayName: 'Test Manager', + getEnvironments: async () => [mockEnv] + } as any; + + // Create mock project + const mockProject: PythonProject = { + uri: vscode.Uri.file('/path/to/project'), + name: 'Test Project' + }; + + mockEnvManagers.setup((em) => em.managers).returns(() => [mockManager]); + mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => [mockProject]); + mockEnvManagers.setup((em) => em.getEnvironment(typeMoq.It.isAny())).returns(() => Promise.resolve(mockEnv)); + + // Verify mocks are set up correctly + const managers = mockEnvManagers.object.managers; + const projects = mockProjectManager.object.getProjects(); + + assert.strictEqual(managers.length, 1); + assert.strictEqual(projects.length, 1); + assert.strictEqual(managers[0].id, 'test-manager'); + assert.strictEqual(projects[0].name, 'Test Project'); + }); + + test('should handle errors gracefully during environment collection', async () => { + const mockManager = { + id: 'error-manager', + displayName: 'Error Manager', + getEnvironments: async () => { + throw new Error('Test error'); + } + } as any; + + mockEnvManagers.setup((em) => em.managers).returns(() => [mockManager]); + mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []); + + // Verify that error conditions don't break the test setup + const managers = mockEnvManagers.object.managers; + assert.strictEqual(managers.length, 1); + assert.strictEqual(managers[0].id, 'error-manager'); + }); + + test('should register report issue command', () => { + // Basic test to ensure command registration structure would work + // The actual command registration happens during extension activation + // This tests the mock setup and basic functionality + + mockEnvManagers.setup((em) => em.managers).returns(() => []); + mockProjectManager.setup((pm) => pm.getProjects(typeMoq.It.isAny())).returns(() => []); + + // Verify basic setup works + assert.notStrictEqual(mockEnvManagers.object, undefined); + assert.notStrictEqual(mockProjectManager.object, undefined); + }); +}); \ No newline at end of file diff --git a/src/test/features/terminal/shells/common/editUtils.unit.test.ts b/src/test/features/terminal/shells/common/editUtils.unit.test.ts new file mode 100644 index 0000000..e228317 --- /dev/null +++ b/src/test/features/terminal/shells/common/editUtils.unit.test.ts @@ -0,0 +1,237 @@ +import * as assert from 'assert'; +import { isWindows } from '../../../../../common/utils/platformUtils'; +import { + hasStartupCode, + insertStartupCode, + removeStartupCode, +} from '../../../../../features/terminal/shells/common/editUtils'; + +suite('Shell Edit Utils', () => { + suite('hasStartupCode', () => { + test('should return false when no markers exist', () => { + const content = 'sample content without markers'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return false when only start marker exists', () => { + const content = 'content\n# START\nsome code'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return false when only end marker exists', () => { + const content = 'content\nsome code\n# END'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return false when markers are in wrong order', () => { + const content = 'content\n# END\nsome code\n# START'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return false when content between markers is empty', () => { + const content = 'content\n# START\n# END\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return false when key is not found between markers', () => { + const content = 'content\n# START\nsome other content\n# END\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, false); + }); + + test('should return true when key is found between markers', () => { + const content = 'content\n# START\nsome key content\n# END\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, true); + }); + + test('should return true when all keys are found between markers', () => { + const content = 'content\n# START\nsome key1 and key2 content\n# END\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key1', 'key2']); + assert.strictEqual(result, true); + }); + + test('should return false when not all keys are found between markers', () => { + const content = 'content\n# START\nsome key1 content\n# END\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key1', 'key2']); + assert.strictEqual(result, false); + }); + + test('should handle Windows line endings (CRLF) correctly', () => { + const content = 'content\r\n# START\r\nsome key content\r\n# END\r\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, true); + }); + + test('should handle mixed line endings correctly', () => { + const content = 'content\n# START\r\nsome key content\n# END\r\nmore content'; + const result = hasStartupCode(content, '# START', '# END', ['key']); + assert.strictEqual(result, true); + }); + }); + + suite('insertStartupCode', () => { + test('should insert code at the end when no markers exist', () => { + const content = 'existing content'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + const lineEndings = isWindows() ? '\r\n' : '\n'; + + const result = insertStartupCode(content, start, end, code); + const expected = `existing content${lineEndings}# START${lineEndings}new code${lineEndings}# END${lineEndings}`; + + assert.strictEqual(result, expected); + }); + + test('should replace code between existing markers', () => { + const content = 'before\n# START\nold code\n# END\nafter'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + + const result = insertStartupCode(content, start, end, code); + const expected = 'before\n# START\nnew code\n# END\nafter'; + + assert.strictEqual(result, expected); + }); + + test('should preserve content outside markers when replacing', () => { + const content = 'line1\nline2\n# START\nold code\n# END\nline3\nline4'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + + const result = insertStartupCode(content, start, end, code); + const expected = 'line1\nline2\n# START\nnew code\n# END\nline3\nline4'; + + assert.strictEqual(result, expected); + }); + + test('should add new code when only start marker exists', () => { + const content = 'before\n# START\nold code'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + + const result = insertStartupCode(content, start, end, code); + const expected = 'before\n# START\nnew code\n# END\n'; + + assert.strictEqual(result, expected); + }); + + test('should add new code when only end marker exists', () => { + const content = 'before\nold code\n# END\nafter'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + + const result = insertStartupCode(content, start, end, code); + const expected = 'before\nold code\n# END\nafter\n# START\nnew code\n# END\n'; + + assert.strictEqual(result, expected); + }); + + test('should handle Windows line endings (CRLF) correctly', () => { + const content = 'before\r\n# START\r\nold code\r\n# END\r\nafter'; + const start = '# START'; + const end = '# END'; + const code = 'new code'; + + const result = insertStartupCode(content, start, end, code); + const expected = 'before\r\n# START\r\nnew code\r\n# END\r\nafter'; + + assert.strictEqual(result, expected); + }); + + test('should preserve original line ending style when inserting', () => { + // Content with Windows line endings + const contentWindows = 'before\r\n# START\r\nold code\r\n# END\r\nafter'; + const resultWindows = insertStartupCode(contentWindows, '# START', '# END', 'new code'); + assert.ok(resultWindows.includes('\r\n'), 'Windows line endings should be preserved'); + + // Content with Unix line endings + const contentUnix = 'before\n# START\nold code\n# END\nafter'; + const resultUnix = insertStartupCode(contentUnix, '# START', '# END', 'new code'); + assert.ok(!resultUnix.includes('\r\n'), 'Unix line endings should be preserved'); + }); + }); + + suite('removeStartupCode', () => { + test('should return original content when no markers exist', () => { + const content = 'sample content without markers'; + const result = removeStartupCode(content, '# START', '# END'); + assert.strictEqual(result, content); + }); + + test('should return original content when only start marker exists', () => { + const content = 'content\n# START\nsome code'; + const result = removeStartupCode(content, '# START', '# END'); + assert.strictEqual(result, content); + }); + + test('should return original content when only end marker exists', () => { + const content = 'content\nsome code\n# END'; + const result = removeStartupCode(content, '# START', '# END'); + assert.strictEqual(result, content); + }); + + test('should return original content when markers are in wrong order', () => { + const content = 'content\n# END\nsome code\n# START'; + const result = removeStartupCode(content, '# START', '# END'); + assert.strictEqual(result, content); + }); + + test('should remove content between markers', () => { + const content = 'before\n# START\ncode to remove\n# END\nafter'; + const result = removeStartupCode(content, '# START', '# END'); + const expected = 'before\nafter'; + assert.strictEqual(result, expected); + }); + + test('should handle multiple lines of content between markers', () => { + const content = 'line1\nline2\n# START\nline3\nline4\nline5\n# END\nline6\nline7'; + const result = removeStartupCode(content, '# START', '# END'); + const expected = 'line1\nline2\nline6\nline7'; + assert.strictEqual(result, expected); + }); + + test('should handle markers at beginning of content', () => { + const content = '# START\ncode to remove\n# END\nafter content'; + const result = removeStartupCode(content, '# START', '# END'); + const expected = 'after content'; + assert.strictEqual(result, expected); + }); + + test('should handle markers at end of content', () => { + const content = 'before content\n# START\ncode to remove\n# END'; + const result = removeStartupCode(content, '# START', '# END'); + const expected = 'before content'; + assert.strictEqual(result, expected); + }); + + test('should handle Windows line endings (CRLF) correctly', () => { + const content = 'before\r\n# START\r\ncode to remove\r\n# END\r\nafter'; + const result = removeStartupCode(content, '# START', '# END'); + const expected = 'before\r\nafter'; + assert.strictEqual(result, expected); + }); + + test('should preserve original line ending style when removing', () => { + // Content with Windows line endings + const contentWindows = 'before\r\n# START\r\ncode to remove\r\n# END\r\nafter'; + const resultWindows = removeStartupCode(contentWindows, '# START', '# END'); + assert.ok(resultWindows.includes('\r\n'), 'Windows line endings should be preserved'); + + // Content with Unix line endings + const contentUnix = 'before\n# START\ncode to remove\n# END\nafter'; + const resultUnix = removeStartupCode(contentUnix, '# START', '# END'); + assert.ok(!resultUnix.includes('\r\n'), 'Unix line endings should be preserved'); + }); + }); +}); diff --git a/src/test/features/terminal/shells/common/shellUtils.unit.test.ts b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts new file mode 100644 index 0000000..12eedfd --- /dev/null +++ b/src/test/features/terminal/shells/common/shellUtils.unit.test.ts @@ -0,0 +1,80 @@ +import * as assert from 'assert'; +import { + extractProfilePath, + PROFILE_TAG_END, + PROFILE_TAG_START, +} from '../../../../../features/terminal/shells/common/shellUtils'; + +suite('Shell Utils', () => { + suite('extractProfilePath', () => { + test('should return undefined when content is empty', () => { + const content = ''; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + + test('should return undefined when content does not have tags', () => { + const content = 'sample content without tags'; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + + test('should return undefined when only start tag exists', () => { + const content = `content\n${PROFILE_TAG_START}\nsome path`; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + + test('should return undefined when only end tag exists', () => { + const content = `content\nsome path\n${PROFILE_TAG_END}`; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + + test('should return undefined when tags are in wrong order', () => { + const content = `content\n${PROFILE_TAG_END}\nsome path\n${PROFILE_TAG_START}`; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + test('should return undefined when content between tags is empty', () => { + const content = `content\n${PROFILE_TAG_START}\n\n${PROFILE_TAG_END}\nmore content`; + const result = extractProfilePath(content); + assert.strictEqual(result, undefined); + }); + + test('should extract path when found between tags', () => { + const expectedPath = '/usr/local/bin/python'; + const content = `content\n${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}\nmore content`; + const result = extractProfilePath(content); + assert.strictEqual(result, expectedPath); + }); + + test('should trim whitespace from extracted path', () => { + const expectedPath = '/usr/local/bin/python'; + const content = `content\n${PROFILE_TAG_START}\n ${expectedPath} \n${PROFILE_TAG_END}\nmore content`; + const result = extractProfilePath(content); + assert.strictEqual(result, expectedPath); + }); + + test('should handle Windows-style line endings', () => { + const expectedPath = 'C:\\Python\\python.exe'; + const content = `content\r\n${PROFILE_TAG_START}\r\n${expectedPath}\r\n${PROFILE_TAG_END}\r\nmore content`; + const result = extractProfilePath(content); + assert.strictEqual(result, expectedPath); + }); + + test('should extract path with special characters', () => { + const expectedPath = '/path with spaces/and (parentheses)/python'; + const content = `${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}`; + const result = extractProfilePath(content); + assert.strictEqual(result, expectedPath); + }); + + test('should extract multiline content correctly', () => { + const expectedPath = 'line1\nline2\nline3'; + const content = `${PROFILE_TAG_START}\n${expectedPath}\n${PROFILE_TAG_END}`; + const result = extractProfilePath(content); + assert.strictEqual(result, expectedPath); + }); + }); +}); diff --git a/src/test/features/terminal/utils.unit.test.ts b/src/test/features/terminal/utils.unit.test.ts new file mode 100644 index 0000000..d97030c --- /dev/null +++ b/src/test/features/terminal/utils.unit.test.ts @@ -0,0 +1,356 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as workspaceApis from '../../../common/workspace.apis'; +import { + ACT_TYPE_COMMAND, + ACT_TYPE_OFF, + ACT_TYPE_SHELL, + AutoActivationType, + getAutoActivationType, +} from '../../../features/terminal/utils'; + +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} + +suite('Terminal Utils - getAutoActivationType', () => { + let mockGetConfiguration: sinon.SinonStub; + let pyEnvsConfig: MockWorkspaceConfig; + let pythonConfig: MockWorkspaceConfig; + + setup(() => { + // Initialize mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + + // Create mock configuration objects + pyEnvsConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + // Set up default configuration returns + mockGetConfiguration.withArgs('python-envs').returns(pyEnvsConfig); + mockGetConfiguration.withArgs('python').returns(pythonConfig); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Priority Order Tests', () => { + test('should return globalRemoteValue when set (highest priority)', () => { + // Mock - globalRemoteValue is set + const mockInspectResult = { + globalRemoteValue: ACT_TYPE_SHELL, + globalLocalValue: ACT_TYPE_COMMAND, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should return globalRemoteValue when set'); + }); + + test('should return globalLocalValue when globalRemoteValue is undefined', () => { + // Mock - globalRemoteValue is undefined, globalLocalValue is set + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_SHELL, + 'Should return globalLocalValue when globalRemoteValue is undefined', + ); + }); + + test('should return globalValue when both globalRemoteValue and globalLocalValue are undefined', () => { + // Mock - only globalValue is set + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: undefined, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_OFF, + 'Should return globalValue when higher priority values are undefined', + ); + }); + + test('should ignore globalLocalValue and globalValue when globalRemoteValue exists', () => { + // Mock - all values set, should prioritize globalRemoteValue + const mockInspectResult = { + globalRemoteValue: ACT_TYPE_OFF, + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_OFF, 'Should prioritize globalRemoteValue over other values'); + }); + + test('should ignore globalValue when globalLocalValue exists', () => { + // Mock - globalLocalValue and globalValue set, should prioritize globalLocalValue + const mockInspectResult = { + globalLocalValue: ACT_TYPE_SHELL, + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should prioritize globalLocalValue over globalValue'); + }); + }); + + suite('Custom Properties Handling', () => { + test('should handle case when globalRemoteValue property does not exist', () => { + // Mock - standard VS Code inspection result without custom properties + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: ACT_TYPE_SHELL, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_SHELL, 'Should return globalValue when custom properties do not exist'); + }); + + test('should handle case when globalLocalValue property does not exist', () => { + // Mock - inspection result without globalLocalValue property + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: ACT_TYPE_COMMAND, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return globalValue when globalLocalValue property does not exist', + ); + }); + + test('should handle case when custom properties exist but are undefined', () => { + // Mock - custom properties exist but have undefined values + const mockInspectResult = { + globalRemoteValue: undefined, + globalLocalValue: undefined, + globalValue: ACT_TYPE_OFF, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_OFF, + 'Should fall back to globalValue when custom properties are undefined', + ); + }); + }); + + suite('Legacy Python Setting Fallback', () => { + test('should return ACT_TYPE_OFF and update config when python.terminal.activateEnvironment is false', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is false + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(false); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_OFF, 'Should return ACT_TYPE_OFF when legacy setting is false'); + assert.ok( + pyEnvsConfig.update.calledWithExactly('terminal.autoActivationType', ACT_TYPE_OFF), + 'Should update python-envs config to ACT_TYPE_OFF', + ); + }); + + test('should return ACT_TYPE_COMMAND when python.terminal.activateEnvironment is true', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is true + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(true); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should return ACT_TYPE_COMMAND when legacy setting is true'); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when legacy setting is true', + ); + }); + + test('should return ACT_TYPE_COMMAND when python.terminal.activateEnvironment is undefined', () => { + // Mock - no python-envs settings, python.terminal.activateEnvironment is undefined + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should return ACT_TYPE_COMMAND when no settings are found'); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when no legacy setting exists', + ); + }); + }); + + suite('Fallback Scenarios', () => { + test('should return ACT_TYPE_COMMAND when no configuration exists', () => { + // Mock - no configurations exist + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(undefined); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return default ACT_TYPE_COMMAND when no configurations exist', + ); + }); + + test('should return ACT_TYPE_COMMAND when python-envs config exists but all values are undefined', () => { + // Mock - python-envs config exists but all relevant values are undefined + const mockInspectResult = { + key: 'terminal.autoActivationType', + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_COMMAND, + 'Should return default when python-envs config exists but values are undefined', + ); + }); + + test('should prioritize python-envs settings over legacy python settings', () => { + // Mock - python-envs has globalValue, python has conflicting setting + const mockInspectResult = { + globalValue: ACT_TYPE_SHELL, + }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(false); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual( + result, + ACT_TYPE_SHELL, + 'Should prioritize python-envs globalValue over legacy python setting', + ); + assert.ok( + pyEnvsConfig.update.notCalled, + 'Should not update python-envs config when it already has a value', + ); + }); + }); + + suite('Edge Cases', () => { + test('should handle null inspect result', () => { + // Mock - inspect returns null + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(null); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should handle null inspect result gracefully'); + }); + + test('should handle empty object inspect result', () => { + // Mock - inspect returns empty object + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns({}); + pythonConfig.get.withArgs('terminal.activateEnvironment', undefined).returns(undefined); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, ACT_TYPE_COMMAND, 'Should handle empty inspect result gracefully'); + }); + + test('should handle all AutoActivationType values correctly', () => { + const testCases: { input: AutoActivationType; expected: AutoActivationType }[] = [ + { input: ACT_TYPE_COMMAND, expected: ACT_TYPE_COMMAND }, + { input: ACT_TYPE_SHELL, expected: ACT_TYPE_SHELL }, + { input: ACT_TYPE_OFF, expected: ACT_TYPE_OFF }, + ]; + + testCases.forEach(({ input, expected }) => { + // Reset stubs for each test case + pyEnvsConfig.inspect.resetHistory(); + pythonConfig.get.resetHistory(); + + // Mock - set globalValue to test input + const mockInspectResult = { globalValue: input }; + pyEnvsConfig.inspect.withArgs('terminal.autoActivationType').returns(mockInspectResult); + + // Run + const result = getAutoActivationType(); + + // Assert + assert.strictEqual(result, expected, `Should handle ${input} value correctly`); + }); + }); + }); +}); diff --git a/src/test/features/terminalEnvVarInjectorBasic.unit.test.ts b/src/test/features/terminalEnvVarInjectorBasic.unit.test.ts new file mode 100644 index 0000000..fc69844 --- /dev/null +++ b/src/test/features/terminalEnvVarInjectorBasic.unit.test.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as typeMoq from 'typemoq'; +import { GlobalEnvironmentVariableCollection, workspace } from 'vscode'; +import { EnvVarManager } from '../../features/execution/envVariableManager'; +import { TerminalEnvVarInjector } from '../../features/terminal/terminalEnvVarInjector'; + +interface MockScopedCollection { + clear: sinon.SinonStub; + replace: sinon.SinonStub; + delete: sinon.SinonStub; +} + +suite('TerminalEnvVarInjector Basic Tests', () => { + let envVarCollection: typeMoq.IMock; + let envVarManager: typeMoq.IMock; + let injector: TerminalEnvVarInjector; + let mockScopedCollection: MockScopedCollection; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let workspaceFoldersStub: any; + + setup(() => { + envVarCollection = typeMoq.Mock.ofType(); + envVarManager = typeMoq.Mock.ofType(); + + // Mock workspace.workspaceFolders property + workspaceFoldersStub = []; + Object.defineProperty(workspace, 'workspaceFolders', { + get: () => workspaceFoldersStub, + configurable: true, + }); + + // Setup scoped collection mock + mockScopedCollection = { + clear: sinon.stub(), + replace: sinon.stub(), + delete: sinon.stub(), + }; + + // Setup environment variable collection to return scoped collection + envVarCollection + .setup((x) => x.getScoped(typeMoq.It.isAny())) + .returns( + () => mockScopedCollection as unknown as ReturnType, + ); + envVarCollection.setup((x) => x.clear()).returns(() => {}); + + // Setup minimal mocks for event subscriptions + envVarManager + .setup((m) => m.onDidChangeEnvironmentVariables) + .returns( + () => + ({ + dispose: () => {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any), + ); + }); + + teardown(() => { + sinon.restore(); + injector?.dispose(); + }); + + test('should initialize without errors', () => { + // Arrange & Act + injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); + + // Assert - should not throw + sinon.assert.match(injector, sinon.match.object); + }); + + test('should dispose cleanly', () => { + // Arrange + injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); + + // Act + injector.dispose(); + + // Assert - should clear on dispose + envVarCollection.verify((c) => c.clear(), typeMoq.Times.atLeastOnce()); + }); + + test('should register environment variable change event handler', () => { + // Arrange + let eventHandlerRegistered = false; + envVarManager.reset(); + envVarManager + .setup((m) => m.onDidChangeEnvironmentVariables) + .returns((_handler) => { + eventHandlerRegistered = true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { dispose: () => {} } as any; + }); + + // Act + injector = new TerminalEnvVarInjector(envVarCollection.object, envVarManager.object); + + // Assert + sinon.assert.match(eventHandlerRegistered, true); + }); +}); diff --git a/src/test/features/views/treeViewItems.unit.test.ts b/src/test/features/views/treeViewItems.unit.test.ts new file mode 100644 index 0000000..929f76f --- /dev/null +++ b/src/test/features/views/treeViewItems.unit.test.ts @@ -0,0 +1,241 @@ +import * as assert from 'assert'; +import { EnvManagerTreeItem, PythonEnvTreeItem } from '../../../features/views/treeViewItems'; +import { InternalEnvironmentManager, PythonEnvironmentImpl } from '../../../internal.api'; +import { Uri } from 'vscode'; + +suite('Test TreeView Items', () => { + suite('EnvManagerTreeItem', () => { + test('Context Value: no-create', () => { + const manager = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + }); + const item = new EnvManagerTreeItem(manager); + assert.equal(item.treeItem.contextValue, 'pythonEnvManager;ms-python.python:test-manager;'); + }); + + test('Context Value: with create', () => { + const manager = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + create: () => Promise.resolve(undefined), + }); + const item = new EnvManagerTreeItem(manager); + assert.equal(item.treeItem.contextValue, 'pythonEnvManager;create;ms-python.python:test-manager;'); + }); + + test('Name is used', () => { + const manager = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + }); + const item = new EnvManagerTreeItem(manager); + assert.equal(item.treeItem.label, manager.name); + }); + + test('DisplayName is used', () => { + const manager = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + displayName: 'Test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + }); + const item = new EnvManagerTreeItem(manager); + assert.equal(item.treeItem.label, manager.displayName); + }); + }); + + suite('PythonEnvTreeItem', () => { + const manager1 = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + displayName: 'Test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + }); + const managerItem1 = new EnvManagerTreeItem(manager1); + + const manager2 = new InternalEnvironmentManager('ms-python.python:test-manager', { + name: 'test', + displayName: 'Test', + description: 'test', + preferredPackageManagerId: 'pip', + refresh: () => Promise.resolve(), + getEnvironments: () => Promise.resolve([]), + resolve: () => Promise.resolve(undefined), + set: () => Promise.resolve(), + get: () => Promise.resolve(undefined), + create: () => Promise.resolve(undefined), + remove: () => Promise.resolve(), + }); + const managerItem2 = new EnvManagerTreeItem(manager2); + + test('Context Value: no-remove, no-activate', () => { + const env = new PythonEnvironmentImpl( + { + id: 'test-env', + managerId: manager1.id, + }, + { + name: 'test-env', + displayName: 'Test Env', + description: 'This is test environment', + displayPath: '/home/user/envs/.venv/bin/python', + version: '3.12.1', + environmentPath: Uri.file('/home/user/envs/.venv/bin/python'), + execInfo: { + run: { + executable: '/home/user/envs/.venv/bin/python', + }, + }, + sysPrefix: '/home/user/envs/.venv', + }, + ); + + const item = new PythonEnvTreeItem(env, managerItem1); + assert.equal(item.treeItem.contextValue, 'pythonEnvironment;'); + }); + + test('Context Value: no-remove, with activate', () => { + const env = new PythonEnvironmentImpl( + { + id: 'test-env', + managerId: manager1.id, + }, + { + name: 'test-env', + displayName: 'Test Env', + description: 'This is test environment', + displayPath: '/home/user/envs/.venv/bin/python', + version: '3.12.1', + environmentPath: Uri.file('/home/user/envs/.venv/bin/python'), + execInfo: { + run: { + executable: '/home/user/envs/.venv/bin/python', + }, + activation: [ + { + executable: '/home/user/envs/.venv/bin/activate', + }, + ], + }, + sysPrefix: '/home/user/envs/.venv', + }, + ); + + const item = new PythonEnvTreeItem(env, managerItem1); + assert.equal(item.treeItem.contextValue, 'pythonEnvironment;activatable;'); + }); + + test('Context Value: with remove, with activate', () => { + const env = new PythonEnvironmentImpl( + { + id: 'test-env', + managerId: manager2.id, + }, + { + name: 'test-env', + displayName: 'Test Env', + description: 'This is test environment', + displayPath: '/home/user/envs/.venv/bin/python', + version: '3.12.1', + environmentPath: Uri.file('/home/user/envs/.venv/bin/python'), + execInfo: { + run: { + executable: '/home/user/envs/.venv/bin/python', + }, + activation: [ + { + executable: '/home/user/envs/.venv/bin/activate', + }, + ], + }, + sysPrefix: '/home/user/envs/.venv', + }, + ); + + const item = new PythonEnvTreeItem(env, managerItem2); + assert.equal(item.treeItem.contextValue, 'pythonEnvironment;remove;activatable;'); + }); + + test('Context Value: with remove, no-activate', () => { + const env = new PythonEnvironmentImpl( + { + id: 'test-env', + managerId: manager2.id, + }, + { + name: 'test-env', + displayName: 'Test Env', + description: 'This is test environment', + displayPath: '/home/user/envs/.venv/bin/python', + version: '3.12.1', + environmentPath: Uri.file('/home/user/envs/.venv/bin/python'), + execInfo: { + run: { + executable: '/home/user/envs/.venv/bin/python', + }, + }, + sysPrefix: '/home/user/envs/.venv', + }, + ); + + const item = new PythonEnvTreeItem(env, managerItem2); + assert.equal(item.treeItem.contextValue, 'pythonEnvironment;remove;'); + }); + + test('Display Name is used', () => { + const env = new PythonEnvironmentImpl( + { + id: 'test-env', + managerId: manager1.id, + }, + { + name: 'test-env', + displayName: 'Test Env', + description: 'This is test environment', + displayPath: '/home/user/envs/.venv/bin/python', + version: '3.12.1', + environmentPath: Uri.file('/home/user/envs/.venv/bin/python'), + execInfo: { + run: { + executable: '/home/user/envs/.venv/bin/python', + }, + }, + sysPrefix: '/home/user/envs/.venv', + }, + ); + + const item = new PythonEnvTreeItem(env, managerItem1); + + assert.equal(item.treeItem.label, env.displayName); + }); + }); +}); diff --git a/src/test/managers/builtin/installArgs.unit.test.ts b/src/test/managers/builtin/installArgs.unit.test.ts new file mode 100644 index 0000000..7a0ddb1 --- /dev/null +++ b/src/test/managers/builtin/installArgs.unit.test.ts @@ -0,0 +1,81 @@ +import assert from 'assert'; +import { processEditableInstallArgs } from '../../../managers/builtin/utils'; + +suite('Process Editable Install Arguments Tests', () => { + test('should handle empty args array', () => { + const result = processEditableInstallArgs([]); + assert.deepStrictEqual(result, [], 'Should return empty array for empty input'); + }); + + test('should pass through non-editable install args unchanged', () => { + const args = ['numpy', 'pandas==2.0.0', '--user']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, args, 'Should return regular args unchanged'); + }); + + test('should pass through single -e argument unchanged', () => { + const args = ['-e', 'c:/path/to/package']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, args, 'Should return single -e arg unchanged'); + }); + + test('should pass through multiple unrelated -e arguments unchanged', () => { + const args = ['-e', 'c:/path/to/package1', '-e', 'c:/path/to/package2']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, args, 'Should return multiple unrelated -e args unchanged'); + }); + + test('should combine -e with extras correctly', () => { + const args = ['-e', 'c:/path/to/package', '-e', '.[testing]']; + const expected = ['-e', 'c:/path/to/package[testing]']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should combine -e with extras correctly'); + }); + + test('should handle multiple editable installs with extras correctly', () => { + const args = ['-e', 'c:/path/to/package1', '-e', '.[testing]', '-e', 'c:/path/to/package2', '-e', '.[dev]']; + const expected = ['-e', 'c:/path/to/package1[testing]', '-e', 'c:/path/to/package2[dev]']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should handle multiple editable installs with extras'); + }); + + test('should handle mixed regular and editable installs correctly', () => { + const args = ['numpy', '-e', 'c:/path/to/package', '-e', '.[testing]', 'pandas==2.0.0']; + const expected = ['numpy', '-e', 'c:/path/to/package[testing]', 'pandas==2.0.0']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should handle mixed regular and editable installs'); + }); + + test('should handle incomplete -e arguments gracefully', () => { + const args = ['-e']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, ['-e'], 'Should handle incomplete -e arguments'); + }); + + test('should not combine -e args when second is not an extras specification', () => { + const args = ['-e', 'c:/path/to/package1', '-e', 'c:/path/to/package2']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, args, 'Should not combine when second -e arg is not an extras spec'); + }); + + test('should handle extras with multiple requirements', () => { + const args = ['-e', 'c:/path/to/package', '-e', '.[testing,dev]']; + const expected = ['-e', 'c:/path/to/package[testing,dev]']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should handle extras with multiple requirements'); + }); + + test('should handle Windows-style paths correctly', () => { + const args = ['-e', 'C:\\path\\to\\package', '-e', '.[testing]']; + const expected = ['-e', 'C:\\path\\to\\package[testing]']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should handle Windows paths correctly'); + }); + + test('should handle editable installs followed by other args', () => { + const args = ['-e', 'c:/path/to/package', '-e', '.[testing]', '--no-deps']; + const expected = ['-e', 'c:/path/to/package[testing]', '--no-deps']; + const result = processEditableInstallArgs(args); + assert.deepStrictEqual(result, expected, 'Should handle editable installs followed by other args'); + }); +}); diff --git a/src/test/managers/builtin/pipListUtils.unit.test.ts b/src/test/managers/builtin/pipListUtils.unit.test.ts new file mode 100644 index 0000000..24bb39d --- /dev/null +++ b/src/test/managers/builtin/pipListUtils.unit.test.ts @@ -0,0 +1,37 @@ +import assert from 'assert'; +import * as fs from 'fs-extra'; +import * as path from 'path'; +import { parsePipList } from '../../../managers/builtin/pipListUtils'; +import { EXTENSION_TEST_ROOT } from '../../constants'; + +const TEST_DATA_ROOT = path.join(EXTENSION_TEST_ROOT, 'managers', 'builtin'); + +suite('Pip List Parser tests', () => { + const testNames = ['piplist1', 'piplist2', 'piplist3']; + + testNames.forEach((testName) => { + test(`Test parsing pip list output ${testName}`, async () => { + const pipListOutput = await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.actual.txt`), 'utf8'); + const expected = JSON.parse( + await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.expected.json`), 'utf8'), + ); + + const actualPackages = parsePipList(pipListOutput); + + assert.equal(actualPackages.length, expected.packages.length, 'Unexpected number of packages'); + actualPackages.forEach((actualPackage) => { + const expectedPackage = expected.packages.find( + (item: { name: string }) => item.name === actualPackage.name, + ); + assert.ok(expectedPackage, `Package ${actualPackage.name} not found in expected packages`); + assert.equal(actualPackage.version, expectedPackage.version, 'Version mismatch'); + }); + + expected.packages.forEach((expectedPackage: { name: string; version: string }) => { + const actualPackage = actualPackages.find((item) => item.name === expectedPackage.name); + assert.ok(actualPackage, `Package ${expectedPackage.name} not found in actual packages`); + assert.equal(actualPackage.version, expectedPackage.version, 'Version mismatch'); + }); + }); + }); +}); diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts new file mode 100644 index 0000000..076596e --- /dev/null +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -0,0 +1,202 @@ +import assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { CancellationToken, Progress, ProgressOptions, Uri } from 'vscode'; +import { PythonEnvironmentApi, PythonProject } from '../../../api'; +import * as winapi from '../../../common/window.apis'; +import * as wapi from '../../../common/workspace.apis'; +import { getProjectInstallable } from '../../../managers/builtin/pipUtils'; + +suite('Pip Utils - getProjectInstallable', () => { + let findFilesStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + // Minimal mock that only implements the methods we need for this test + // Using type assertion to satisfy TypeScript since we only need getPythonProject + let mockApi: { getPythonProject: (uri: Uri) => PythonProject | undefined }; + + setup(() => { + findFilesStub = sinon.stub(wapi, 'findFiles'); + // Stub withProgress to immediately execute the callback + withProgressStub = sinon.stub(winapi, 'withProgress'); + withProgressStub.callsFake( + async ( + _options: ProgressOptions, + callback: ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, + ) => Thenable, + ) => { + return await callback( + {} as Progress<{ message?: string; increment?: number }>, + { isCancellationRequested: false } as CancellationToken, + ); + }, + ); + + const workspacePath = Uri.file('/test/path/root').fsPath; + mockApi = { + getPythonProject: (uri: Uri) => { + // Return a project for any URI in workspace + if (uri.fsPath.startsWith(workspacePath)) { + return { name: 'workspace', uri: Uri.file(workspacePath) }; + } + return undefined; + }, + }; + }); + + teardown(() => { + sinon.restore(); + }); + + test('should find dev-requirements.txt at workspace root', async () => { + // Arrange: Mock findFiles to return both requirements.txt and dev-requirements.txt + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + // This pattern might not match root-level files in VS Code + return Promise.resolve([]); + } else if (pattern === '*requirements*.txt') { + // This pattern should match root-level files + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([ + Uri.file(path.join(workspacePath, 'requirements.txt')), + Uri.file(path.join(workspacePath, 'dev-requirements.txt')), + Uri.file(path.join(workspacePath, 'test-requirements.txt')), + ]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const workspacePath = Uri.file('/test/path/root').fsPath; + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); + + // Assert: Should find all three requirements files + assert.strictEqual(result.length, 3, 'Should find three requirements files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual( + names, + ['dev-requirements.txt', 'requirements.txt', 'test-requirements.txt'], + 'Should find requirements.txt, dev-requirements.txt, and test-requirements.txt', + ); + + // Verify each file has correct properties + result.forEach((item) => { + assert.strictEqual(item.group, 'Requirements', 'Should be in Requirements group'); + assert.ok(item.args, 'Should have args'); + assert.strictEqual(item.args?.length, 2, 'Should have 2 args'); + assert.strictEqual(item.args?.[0], '-r', 'First arg should be -r'); + assert.ok(item.uri, 'Should have a URI'); + }); + }); + + test('should deduplicate files found by multiple patterns', async () => { + // Arrange: Mock both patterns to return the same file + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([Uri.file(path.join(workspacePath, 'dev-requirements.txt'))]); + } else if (pattern === '*requirements*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([ + Uri.file(path.join(workspacePath, 'dev-requirements.txt')), + Uri.file(path.join(workspacePath, 'requirements.txt')), + ]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const workspacePath = Uri.file('/test/path/root').fsPath; + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); + + // Assert: Should deduplicate and only have 2 unique files + assert.strictEqual(result.length, 2, 'Should deduplicate and have 2 unique files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual(names, ['dev-requirements.txt', 'requirements.txt'], 'Should have deduplicated results'); + }); + + test('should find requirements files in subdirectories', async () => { + // Arrange: Mock findFiles to return files in subdirectories + findFilesStub.callsFake((pattern: string) => { + if (pattern === '**/*requirements*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([Uri.file(path.join(workspacePath, 'subdir', 'dev-requirements.txt'))]); + } else if (pattern === '*requirements*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements.txt'))]); + } else if (pattern === '**/requirements/*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements', 'test.txt'))]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act: Call getProjectInstallable + const workspacePath = Uri.file('/test/path/root').fsPath; + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); + + // Assert: Should find all files + assert.strictEqual(result.length, 3, 'Should find three files'); + + const names = result.map((r) => r.name).sort(); + assert.deepStrictEqual( + names, + ['dev-requirements.txt', 'requirements.txt', 'test.txt'], + 'Should find files at different levels', + ); + }); + + test('should return empty array when no projects provided', async () => { + // Act: Call with no projects + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, undefined); + + // Assert: Should return empty array + assert.strictEqual(result.length, 0, 'Should return empty array'); + assert.ok(!findFilesStub.called, 'Should not call findFiles when no projects'); + }); + + test('should filter out files not in project directories', async () => { + // Arrange: Mock findFiles to return files from multiple directories + findFilesStub.callsFake((pattern: string) => { + if (pattern === '*requirements*.txt') { + const workspacePath = Uri.file('/test/path/root').fsPath; + const otherPath = Uri.file('/other-dir').fsPath; + return Promise.resolve([ + Uri.file(path.join(workspacePath, 'requirements.txt')), + Uri.file(path.join(otherPath, 'requirements.txt')), // Should be filtered out + ]); + } else { + return Promise.resolve([]); + } + }); + + // Act: Call with only workspace project + const workspacePath = Uri.file('/test/path/root').fsPath; + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = await getProjectInstallable(mockApi as PythonEnvironmentApi, projects); + + // Assert: Should only include files from workspace + assert.strictEqual(result.length, 1, 'Should only include files from project directory'); + const firstResult = result[0]; + assert.ok(firstResult, 'Should have at least one result'); + assert.strictEqual(firstResult.name, 'requirements.txt'); + assert.ok(firstResult.uri, 'Should have a URI'); + assert.ok(firstResult.uri.fsPath.startsWith(workspacePath), 'Should be in workspace directory'); + }); +}); diff --git a/src/test/managers/builtin/piplist1.actual.txt b/src/test/managers/builtin/piplist1.actual.txt new file mode 100644 index 0000000..7d29157 --- /dev/null +++ b/src/test/managers/builtin/piplist1.actual.txt @@ -0,0 +1,38 @@ +Package Version +------------------ -------- +argcomplete 3.1.2 +black 23.12.1 +build 1.2.1 +click 8.1.7 +colorama 0.4.6 +colorlog 6.7.0 +coverage 7.6.1 +distlib 0.3.7 +exceptiongroup 1.1.3 +filelock 3.13.1 +importlib_metadata 7.1.0 +iniconfig 2.0.0 +isort 5.13.2 +mypy-extensions 1.0.0 +namedpipe 0.1.1 +nox 2024.3.2 +packaging 23.2 +pathspec 0.12.1 +pip 24.0 +pip-tools 7.4.1 +platformdirs 3.11.0 +pluggy 1.4.0 +pyproject_hooks 1.1.0 +pytest 8.1.1 +pytest-cov 5.0.0 +pywin32 306 +ruff 0.7.4 +setuptools 56.0.0 +tomli 2.0.1 +typing_extensions 4.9.0 +virtualenv 20.24.6 +wheel 0.43.0 +zipp 3.19.2 + +[notice] A new release of pip is available: 24.0 -> 24.3.1 +[notice] To update, run: python.exe -m pip install --upgrade pip \ No newline at end of file diff --git a/src/test/managers/builtin/piplist1.expected.json b/src/test/managers/builtin/piplist1.expected.json new file mode 100644 index 0000000..50234f5 --- /dev/null +++ b/src/test/managers/builtin/piplist1.expected.json @@ -0,0 +1,37 @@ +{ + "packages": [ + { "name": "argcomplete", "version": "3.1.2" }, + { "name": "black", "version": "23.12.1" }, + { "name": "build", "version": "1.2.1" }, + { "name": "click", "version": "8.1.7" }, + { "name": "colorama", "version": "0.4.6" }, + { "name": "colorlog", "version": "6.7.0" }, + { "name": "coverage", "version": "7.6.1" }, + { "name": "distlib", "version": "0.3.7" }, + { "name": "exceptiongroup", "version": "1.1.3" }, + { "name": "filelock", "version": "3.13.1" }, + { "name": "importlib_metadata", "version": "7.1.0" }, + { "name": "iniconfig", "version": "2.0.0" }, + { "name": "isort", "version": "5.13.2" }, + { "name": "mypy-extensions", "version": "1.0.0" }, + { "name": "namedpipe", "version": "0.1.1" }, + { "name": "nox", "version": "2024.3.2" }, + { "name": "packaging", "version": "23.2" }, + { "name": "pathspec", "version": "0.12.1" }, + { "name": "pip", "version": "24.0" }, + { "name": "pip-tools", "version": "7.4.1" }, + { "name": "platformdirs", "version": "3.11.0" }, + { "name": "pluggy", "version": "1.4.0" }, + { "name": "pyproject_hooks", "version": "1.1.0" }, + { "name": "pytest", "version": "8.1.1" }, + { "name": "pytest-cov", "version": "5.0.0" }, + { "name": "pywin32", "version": "306" }, + { "name": "ruff", "version": "0.7.4" }, + { "name": "setuptools", "version": "56.0.0" }, + { "name": "tomli", "version": "2.0.1" }, + { "name": "typing_extensions", "version": "4.9.0" }, + { "name": "virtualenv", "version": "20.24.6" }, + { "name": "wheel", "version": "0.43.0" }, + { "name": "zipp", "version": "3.19.2" } + ] +} diff --git a/src/test/managers/builtin/piplist2.actual.txt b/src/test/managers/builtin/piplist2.actual.txt new file mode 100644 index 0000000..5cefab6 --- /dev/null +++ b/src/test/managers/builtin/piplist2.actual.txt @@ -0,0 +1,35 @@ +Package Version +------------------ -------- +argcomplete 3.1.2 +black 23.12.1 +build 1.2.1 +click 8.1.7 +colorama 0.4.6 +colorlog 6.7.0 +coverage 7.6.1 +distlib 0.3.7 +exceptiongroup 1.1.3 +filelock 3.13.1 +importlib_metadata 7.1.0 +iniconfig 2.0.0 +isort 5.13.2 +mypy-extensions 1.0.0 +namedpipe 0.1.1 +nox 2024.3.2 +packaging 23.2 +pathspec 0.12.1 +pip 24.0 +pip-tools 7.4.1 +platformdirs 3.11.0 +pluggy 1.4.0 +pyproject_hooks 1.1.0 +pytest 8.1.1 +pytest-cov 5.0.0 +pywin32 306 +ruff 0.7.4 +setuptools 56.0.0 +tomli 2.0.1 +typing_extensions 4.9.0 +virtualenv 20.24.6 +wheel 0.43.0 +zipp 3.19.2 \ No newline at end of file diff --git a/src/test/managers/builtin/piplist2.expected.json b/src/test/managers/builtin/piplist2.expected.json new file mode 100644 index 0000000..50234f5 --- /dev/null +++ b/src/test/managers/builtin/piplist2.expected.json @@ -0,0 +1,37 @@ +{ + "packages": [ + { "name": "argcomplete", "version": "3.1.2" }, + { "name": "black", "version": "23.12.1" }, + { "name": "build", "version": "1.2.1" }, + { "name": "click", "version": "8.1.7" }, + { "name": "colorama", "version": "0.4.6" }, + { "name": "colorlog", "version": "6.7.0" }, + { "name": "coverage", "version": "7.6.1" }, + { "name": "distlib", "version": "0.3.7" }, + { "name": "exceptiongroup", "version": "1.1.3" }, + { "name": "filelock", "version": "3.13.1" }, + { "name": "importlib_metadata", "version": "7.1.0" }, + { "name": "iniconfig", "version": "2.0.0" }, + { "name": "isort", "version": "5.13.2" }, + { "name": "mypy-extensions", "version": "1.0.0" }, + { "name": "namedpipe", "version": "0.1.1" }, + { "name": "nox", "version": "2024.3.2" }, + { "name": "packaging", "version": "23.2" }, + { "name": "pathspec", "version": "0.12.1" }, + { "name": "pip", "version": "24.0" }, + { "name": "pip-tools", "version": "7.4.1" }, + { "name": "platformdirs", "version": "3.11.0" }, + { "name": "pluggy", "version": "1.4.0" }, + { "name": "pyproject_hooks", "version": "1.1.0" }, + { "name": "pytest", "version": "8.1.1" }, + { "name": "pytest-cov", "version": "5.0.0" }, + { "name": "pywin32", "version": "306" }, + { "name": "ruff", "version": "0.7.4" }, + { "name": "setuptools", "version": "56.0.0" }, + { "name": "tomli", "version": "2.0.1" }, + { "name": "typing_extensions", "version": "4.9.0" }, + { "name": "virtualenv", "version": "20.24.6" }, + { "name": "wheel", "version": "0.43.0" }, + { "name": "zipp", "version": "3.19.2" } + ] +} diff --git a/src/test/managers/builtin/piplist3.actual.txt b/src/test/managers/builtin/piplist3.actual.txt new file mode 100644 index 0000000..4450b42 --- /dev/null +++ b/src/test/managers/builtin/piplist3.actual.txt @@ -0,0 +1,11 @@ +Package Version +---------- ------- +altgraph 0.17.2 +future 0.18.2 +macholib 1.15.2 +pip 21.2.4 +setuptools 58.0.4 +six 1.15.0 +wheel 0.37.0 +WARNING: You are using pip version 21.2.4; however, version 25.2 is available. +You should consider upgrading via the '/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip' command. diff --git a/src/test/managers/builtin/piplist3.expected.json b/src/test/managers/builtin/piplist3.expected.json new file mode 100644 index 0000000..c22fcbe --- /dev/null +++ b/src/test/managers/builtin/piplist3.expected.json @@ -0,0 +1,11 @@ +{ + "packages": [ + { "name": "altgraph", "version": "0.17.2" }, + { "name": "future", "version": "0.18.2" }, + { "name": "macholib", "version": "1.15.2" }, + { "name": "pip", "version": "21.2.4" }, + { "name": "setuptools", "version": "58.0.4" }, + { "name": "six", "version": "1.15.0" }, + { "name": "wheel", "version": "0.37.0" } + ] +} diff --git a/src/test/managers/common/nativePythonFinder.getAllExtraSearchPaths.unit.test.ts b/src/test/managers/common/nativePythonFinder.getAllExtraSearchPaths.unit.test.ts new file mode 100644 index 0000000..3a83b3a --- /dev/null +++ b/src/test/managers/common/nativePythonFinder.getAllExtraSearchPaths.unit.test.ts @@ -0,0 +1,497 @@ +import assert from 'node:assert'; +import path from 'node:path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// Import the function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} + +suite('getAllExtraSearchPaths Integration Tests', () => { + let mockGetConfiguration: sinon.SinonStub; + let mockUntildify: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + + // Mock configuration objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // Mock VS Code workspace APIs + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockUntildify = sinon.stub(pathUtils, 'untildify'); + // Also stub the namespace import version that might be used by untildifyArray + sinon + .stub(pathUtils, 'untildifyArray') + .callsFake((paths: string[]) => + paths.map((p) => (p.startsWith('~/') ? p.replace('~/', '/home/user/') : p)), + ); + + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // Default workspace behavior - no folders + mockGetWorkspaceFolders.returns(undefined); + + // Create mock configuration objects + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + // Default untildify behavior - expand tildes to test paths + mockUntildify.callsFake((path: string) => { + if (path.startsWith('~/')) { + return path.replace('~/', '/home/user/'); + } + return path; + }); + + // Set up default returns for legacy settings (return undefined by default) + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + + // Set up default returns for new settings + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Default configuration behavior + mockGetConfiguration.callsFake((section: string, _scope?: unknown) => { + if (section === 'python') { + return pythonConfig; + } + if (section === 'python-env') { + return envConfig; + } + throw new Error(`Unexpected configuration section: ${section}`); + }); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Legacy Path Consolidation Tests', () => { + test('No legacy settings exist - returns empty paths', async () => { + // Mock → No legacy settings, no new settings + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + assert.deepStrictEqual(result, []); + }); + + test('Legacy and global paths are consolidated', async () => { + // Mock → Legacy paths and globalSearchPaths both exist + pythonConfig.get.withArgs('venvPath').returns('/home/user/.virtualenvs'); + pythonConfig.get.withArgs('venvFolders').returns(['/home/user/venvs']); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['/home/user/.virtualenvs', '/home/user/venvs', '/additional/path'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Should consolidate all paths (duplicates removed) + const expected = new Set(['/home/user/.virtualenvs', '/home/user/venvs', '/additional/path']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Legacy paths included alongside new settings', async () => { + // Mock → Legacy paths exist, no globalSearchPaths + pythonConfig.get.withArgs('venvPath').returns('/home/user/.virtualenvs'); + pythonConfig.get.withArgs('venvFolders').returns(['/home/user/venvs', '/home/user/conda']); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Should include all legacy paths + const expected = new Set(['/home/user/.virtualenvs', '/home/user/venvs', '/home/user/conda']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Legacy and global paths combined with deduplication', async () => { + // Mock → Some overlap between legacy and global paths + pythonConfig.get.withArgs('venvPath').returns('/home/user/.virtualenvs'); + pythonConfig.get.withArgs('venvFolders').returns(['/home/user/venvs', '/home/user/conda']); + envConfig.inspect + .withArgs('globalSearchPaths') + .returns({ globalValue: ['/home/user/.virtualenvs', '/additional/path'] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Should include all paths with duplicates removed + const expected = new Set([ + '/home/user/.virtualenvs', + '/home/user/venvs', + '/home/user/conda', + '/additional/path', + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Legacy paths with untildify support', async () => { + // Mock → Legacy paths with tilde expansion + // Note: getPythonSettingAndUntildify only untildifies strings, not array items + // So we return the venvPath with tilde (will be untildified) and venvFolders pre-expanded + pythonConfig.get.withArgs('venvPath').returns('~/virtualenvs'); + pythonConfig.get.withArgs('venvFolders').returns(['/home/user/conda/envs']); // Pre-expanded + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + const expected = new Set(['/home/user/virtualenvs', '/home/user/conda/envs']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + }); + + suite('Configuration Source Tests', () => { + test('Global search paths with tilde expansion', async () => { + // Mock → No legacy, global paths with tildes + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['~/virtualenvs', '~/conda/envs'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + mockUntildify.withArgs('~/virtualenvs').returns('/home/user/virtualenvs'); + mockUntildify.withArgs('~/conda/envs').returns('/home/user/conda/envs'); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + const expected = new Set(['/home/user/virtualenvs', '/home/user/conda/envs']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Workspace folder setting preferred over workspace setting', async () => { + // Mock → Workspace settings at different levels + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceValue: ['workspace-level-path'], + workspaceFolderValue: ['folder-level-path'], + }); + + const workspace1 = Uri.file('/workspace/project1'); + const workspace2 = Uri.file('/workspace/project2'); + mockGetWorkspaceFolders.returns([{ uri: workspace1 }, { uri: workspace2 }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Use dynamic path construction based on actual workspace URIs + const expected = new Set([ + path.resolve(workspace1.fsPath, 'folder-level-path'), + path.resolve(workspace2.fsPath, 'folder-level-path'), + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Global workspace setting logs error and is ignored', async () => { + // Mock → Workspace setting incorrectly set at global level + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + globalValue: ['should-be-ignored'], + }); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + assert.deepStrictEqual(result, []); + // Check that error was logged with key terms - don't be brittle about exact wording + assert( + mockTraceError.calledWith(sinon.match(/workspaceSearchPaths.*global.*level/i)), + 'Should log error about incorrect setting level', + ); + }); + + test('Configuration read errors return empty arrays', async () => { + // Mock → Configuration throws errors + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').throws(new Error('Config read error')); + envConfig.inspect.withArgs('workspaceSearchPaths').throws(new Error('Config read error')); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + assert.deepStrictEqual(result, []); + // Just verify that configuration errors were logged - don't be brittle about exact wording + assert( + mockTraceError.calledWith(sinon.match(/globalSearchPaths/i), sinon.match.instanceOf(Error)), + 'Should log globalSearchPaths error', + ); + assert( + mockTraceError.calledWith(sinon.match(/workspaceSearchPaths/i), sinon.match.instanceOf(Error)), + 'Should log workspaceSearchPaths error', + ); + }); + }); + + suite('Path Resolution Tests', () => { + test('Absolute paths used as-is', async () => { + // Mock → Mix of absolute paths + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['/absolute/path1', '/absolute/path2'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['/absolute/workspace/path'], + }); + + const workspace = Uri.file('/workspace'); + mockGetWorkspaceFolders.returns([{ uri: workspace }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - For absolute paths, they should remain unchanged regardless of platform + const expected = new Set(['/absolute/path1', '/absolute/path2', '/absolute/workspace/path']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Relative paths resolved against workspace folders', async () => { + // Mock → Relative workspace paths with multiple workspace folders + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['venvs', '../shared-envs'], + }); + + const workspace1 = Uri.file('/workspace/project1'); + const workspace2 = Uri.file('/workspace/project2'); + mockGetWorkspaceFolders.returns([{ uri: workspace1 }, { uri: workspace2 }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - path.resolve() correctly resolves relative paths (order doesn't matter) + const expected = new Set([ + path.resolve(workspace1.fsPath, 'venvs'), + path.resolve(workspace2.fsPath, 'venvs'), + path.resolve(workspace1.fsPath, '../shared-envs'), // Resolves against workspace1 + path.resolve(workspace2.fsPath, '../shared-envs'), // Resolves against workspace2 + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Relative paths without workspace folders logs warning', async () => { + // Mock → Relative paths but no workspace folders + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['relative-path'], + }); + + mockGetWorkspaceFolders.returns(undefined); // No workspace folders + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + assert.deepStrictEqual(result, []); + // Check that warning was logged with key terms - don't be brittle about exact wording + assert( + mockTraceWarn.calledWith(sinon.match(/workspace.*folder.*relative.*path/i), 'relative-path'), + 'Should log warning about missing workspace folders', + ); + }); + + test('Empty and whitespace paths are skipped', async () => { + // Mock → Mix of valid and invalid paths + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['/valid/path', '', ' ', '/another/valid/path'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['valid-relative', '', ' \t\n ', 'another-valid'], + }); + + const workspace = Uri.file('/workspace'); + mockGetWorkspaceFolders.returns([{ uri: workspace }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Now globalSearchPaths empty strings should be filtered out (order doesn't matter) + const expected = new Set([ + '/valid/path', + '/another/valid/path', + path.resolve(workspace.fsPath, 'valid-relative'), + path.resolve(workspace.fsPath, 'another-valid'), + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + }); + + suite('Integration Scenarios', () => { + test('Fresh install - no settings configured', async () => { + // Mock → Clean slate + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({}); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert + assert.deepStrictEqual(result, []); + }); + + test('Power user - complex mix of all source types', async () => { + // Mock → Complex real-world scenario + pythonConfig.get.withArgs('venvPath').returns('/legacy/venv/path'); + pythonConfig.get.withArgs('venvFolders').returns(['/legacy/venvs']); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['/legacy/venv/path', '/legacy/venvs', '/global/conda', '~/personal/envs'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['.venv', 'project-envs', '/shared/team/envs'], + }); + + const workspace1 = Uri.file('/workspace/project1'); + const workspace2 = Uri.file('/workspace/project2'); + mockGetWorkspaceFolders.returns([{ uri: workspace1 }, { uri: workspace2 }]); + + mockUntildify.withArgs('~/personal/envs').returns('/home/user/personal/envs'); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Should deduplicate and combine all sources (order doesn't matter) + const expected = new Set([ + '/legacy/venv/path', + '/legacy/venvs', + '/global/conda', + '/home/user/personal/envs', + path.resolve(workspace1.fsPath, '.venv'), + path.resolve(workspace2.fsPath, '.venv'), + path.resolve(workspace1.fsPath, 'project-envs'), + path.resolve(workspace2.fsPath, 'project-envs'), + '/shared/team/envs', + ]); + const actual = new Set(result); + + // Check that we have exactly the expected paths (no more, no less) + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('Overlapping paths are deduplicated', async () => { + // Mock → Duplicate paths from different sources + pythonConfig.get.withArgs('venvPath').returns(undefined); + pythonConfig.get.withArgs('venvFolders').returns(undefined); + envConfig.inspect.withArgs('globalSearchPaths').returns({ + globalValue: ['/shared/path', '/global/unique'], + }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['/shared/path', 'workspace-unique'], + }); + + const workspace = Uri.file('/workspace'); + mockGetWorkspaceFolders.returns([{ uri: workspace }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Duplicates should be removed (order doesn't matter) + const expected = new Set([ + '/shared/path', + '/global/unique', + path.resolve(workspace.fsPath, 'workspace-unique'), + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + + test('All path types consolidated together', async () => { + // Mock → Multiple path types from different sources + pythonConfig.get.withArgs('venvPath').returns('/legacy/path'); + pythonConfig.get.withArgs('venvFolders').returns(['/legacy/folder']); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: ['/global/path'] }); + envConfig.inspect.withArgs('workspaceSearchPaths').returns({ + workspaceFolderValue: ['workspace-relative'], + }); + + const workspace = Uri.file('/workspace'); + mockGetWorkspaceFolders.returns([{ uri: workspace }]); + + // Run + const result = await getAllExtraSearchPaths(); + + // Assert - Should consolidate all path types + const expected = new Set([ + '/legacy/path', + '/legacy/folder', + '/global/path', + path.resolve(workspace.fsPath, 'workspace-relative'), + ]); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of unique paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + }); + }); +}); diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 0000000..b612aaa --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,27 @@ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as TypeMoq from 'typemoq'; +import { Readable } from 'stream'; +import * as common from 'typemoq/Common/_all'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} + +export function createTypeMoq( + targetCtor?: common.CtorWithArgs, + behavior?: TypeMoq.MockBehavior, + shouldOverrideTarget?: boolean, + ...targetCtorArgs: any[] +): TypeMoq.IMock { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = TypeMoq.Mock.ofType(targetCtor, behavior, shouldOverrideTarget, ...targetCtorArgs); + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} diff --git a/src/test/mocks/mementos.ts b/src/test/mocks/mementos.ts new file mode 100644 index 0000000..4cae66c --- /dev/null +++ b/src/test/mocks/mementos.ts @@ -0,0 +1,34 @@ +import { Memento } from 'vscode'; + +export class MockMemento implements Memento { + // Note: This has to be called _value so that it matches + // what VS code has for a memento. We use this to eliminate a bad bug + // with writing too much data to global storage. See bug https://github.com/microsoft/vscode-python/issues/9159 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _value: Record = {}; + + public keys(): string[] { + return Object.keys(this._value); + } + + // @ts-ignore Ignore the return value warning + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public get(key: any, defaultValue?: any); + + public get(key: string, defaultValue?: T): T { + const exists = this._value.hasOwnProperty(key); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return exists ? this._value[key] : (defaultValue! as any); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public update(key: string, value: any): Thenable { + this._value[key] = value; + return Promise.resolve(); + } + + public clear(): void { + this._value = {}; + } +} diff --git a/src/test/mocks/mockChildProcess.ts b/src/test/mocks/mockChildProcess.ts new file mode 100644 index 0000000..33cac2a --- /dev/null +++ b/src/test/mocks/mockChildProcess.ts @@ -0,0 +1,240 @@ + +/* 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; + } + + disconnect(): void { + /* noop */ + } + + unref(): void { + /* noop */ + } + + 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: unknown[] = 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; + } + + dispose(): void { + this.stdout?.destroy(); + } +} diff --git a/src/test/mocks/mockDocument.ts b/src/test/mocks/mockDocument.ts new file mode 100644 index 0000000..6875fce --- /dev/null +++ b/src/test/mocks/mockDocument.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; + +class MockLine implements TextLine { + private _range: Range; + + private _rangeWithLineBreak: Range; + + private _firstNonWhitespaceIndex: number | undefined; + + private _isEmpty: boolean | undefined; + + constructor(private _contents: string, private _line: number, private _offset: number) { + this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); + this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); + } + + public get offset(): number { + return this._offset; + } + + public get lineNumber(): number { + return this._line; + } + + public get text(): string { + return this._contents; + } + + public get range(): Range { + return this._range; + } + + public get rangeIncludingLineBreak(): Range { + return this._rangeWithLineBreak; + } + + public get firstNonWhitespaceCharacterIndex(): number { + if (this._firstNonWhitespaceIndex === undefined) { + this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; + } + return this._firstNonWhitespaceIndex; + } + + public get isEmptyOrWhitespace(): boolean { + if (this._isEmpty === undefined) { + this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; + } + return this._isEmpty; + } +} + +export class MockDocument implements TextDocument { + private _uri: Uri; + + private _version = 0; + + private _lines: MockLine[] = []; + + private _contents = ''; + + private _isUntitled = false; + + private _isDirty = false; + + private _language = 'python'; + + private _onSave: (doc: TextDocument) => Promise; + + encoding = 'utf-8'; + + constructor( + contents: string, + fileName: string, + onSave: (doc: TextDocument) => Promise, + language?: string, + ) { + this._uri = Uri.file(fileName); + this._contents = contents; + this._lines = this.createLines(); + this._onSave = onSave; + this._language = language ?? this._language; + } + + public setContent(contents: string): void { + this._contents = contents; + this._lines = this.createLines(); + } + + public addContent(contents: string): void { + this.setContent(`${this._contents}\n${contents}`); + } + + public forceUntitled(): void { + this._isUntitled = true; + this._isDirty = true; + } + + public get uri(): Uri { + return this._uri; + } + + public get fileName(): string { + return this._uri.fsPath; + } + + public get isUntitled(): boolean { + return this._isUntitled; + } + + public get languageId(): string { + return this._language; + } + + public get version(): number { + return this._version; + } + + public get isDirty(): boolean { + return this._isDirty; + } + + public get isClosed(): boolean { + return false; + } + + public save(): Thenable { + return this._onSave(this); + } + + public get eol(): EndOfLine { + return EndOfLine.LF; + } + + public get lineCount(): number { + return this._lines.length; + } + + public lineAt(position: Position | number): TextLine { + if (typeof position === 'number') { + return this._lines[position as number]; + } + return this._lines[position.line]; + } + + public offsetAt(position: Position): number { + return this.convertToOffset(position); + } + + public positionAt(offset: number): Position { + let line = 0; + let ch = 0; + while (line + 1 < this._lines.length && this._lines[line + 1].offset <= offset) { + line += 1; + } + if (line < this._lines.length) { + ch = offset - this._lines[line].offset; + } + return new Position(line, ch); + } + + public getText(range?: Range | undefined): string { + if (!range) { + return this._contents; + } + const startOffset = this.convertToOffset(range.start); + const endOffset = this.convertToOffset(range.end); + return this._contents.substr(startOffset, endOffset - startOffset); + } + + public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { + if (!regexp && position.line > 0) { + // use default when custom-regexp isn't provided + regexp = /a/; + } + + return undefined; + } + + public validateRange(range: Range): Range { + return range; + } + + public validatePosition(position: Position): Position { + return position; + } + + public edit(c: TextDocumentContentChangeEvent): void { + this._version += 1; + const before = this._contents.substr(0, c.rangeOffset); + const after = this._contents.substr(c.rangeOffset + c.rangeLength); + this._contents = `${before}${c.text}${after}`; + this._lines = this.createLines(); + } + + private createLines(): MockLine[] { + const split = this._contents.split('\n'); + let prevLine: MockLine | undefined; + return split.map((s, i) => { + const nextLine = this.createTextLine(s, i, prevLine); + prevLine = nextLine; + return nextLine; + }); + } + + private createTextLine(line: string, index: number, prevLine: MockLine | undefined): MockLine { + return new MockLine( + line, + index, + prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0, + ); + } + + private convertToOffset(pos: Position): number { + if (pos.line < this._lines.length) { + return ( + this._lines[pos.line].offset + + Math.min(this._lines[pos.line].rangeIncludingLineBreak.end.character, pos.character) + ); + } + return this._contents.length; + } +} diff --git a/src/test/mocks/mockWorkspaceConfig.ts b/src/test/mocks/mockWorkspaceConfig.ts new file mode 100644 index 0000000..a1d151e --- /dev/null +++ b/src/test/mocks/mockWorkspaceConfig.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; + +type SectionType = { + key: string; + defaultValue?: T | undefined; + globalValue?: T | undefined; + globalLanguageValue?: T | undefined; + workspaceValue?: T | undefined; + workspaceLanguageValue?: T | undefined; + workspaceFolderValue?: T | undefined; + workspaceFolderLanguageValue?: T | undefined; +}; + +export class MockWorkspaceConfiguration implements WorkspaceConfiguration { + private values = new Map(); + + constructor(defaultSettings?: { [key: string]: unknown }) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach((k) => this.values.set(k, defaultSettings[k])); + } + } + + public get(key: string, defaultValue?: T): T | undefined { + if (this.values.has(key)) { + return this.values.get(key) as T; + } + + return arguments.length > 1 ? defaultValue : undefined; + } + + public has(section: string): boolean { + return this.values.has(section); + } + + public inspect(section: string): SectionType | undefined { + return this.values.get(section) as SectionType; + } + + public update( + section: string, + value: unknown, + + _configurationTarget?: boolean | ConfigurationTarget | undefined, + ): Promise { + this.values.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/mocks/vsc/README.md b/src/test/mocks/vsc/README.md new file mode 100644 index 0000000..2803528 --- /dev/null +++ b/src/test/mocks/vsc/README.md @@ -0,0 +1,7 @@ +# This folder contains classes exposed by VS Code required in running the unit tests. + +- These classes are only used when running unit tests that are not hosted by VS Code. +- So even if these classes were buggy, it doesn't matter, running the tests under VS Code host will ensure the right classes are available. +- The purpose of these classes are to avoid having to use VS Code as the hosting environment for the tests, making it faster to run the tests and not have to rely on VS Code host to run the tests. +- Everything in here must either be within a namespace prefixed with `vscMock` or exported types must be prefixed with `vscMock`. + This is to prevent developers from accidentally importing them into their Code. Even if they did, the extension would fail to load and tests would fail. diff --git a/src/test/mocks/vsc/arrays.ts b/src/test/mocks/vsc/arrays.ts new file mode 100644 index 0000000..45624a2 --- /dev/null +++ b/src/test/mocks/vsc/arrays.ts @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Returns the last element of an array. + * @param array The array. + * @param n Which element from the end (default is zero). + */ +export function tail(array: T[], n = 0): T { + return array[array.length - (1 + n)]; +} + +export function equals(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i += 1) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; +} + +export function binarySearch(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { + let low = 0; + let high = array.length - 1; + + while (low <= high) { + const mid = ((low + high) / 2) | 0; + const comp = comparator(array[mid], key); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } + } + return -(low + 1); +} + +/** + * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false + * are located before all elements where p(x) is true. + * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. + */ +export function findFirst(array: T[], p: (x: T) => boolean): number { + let low = 0; + let high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; + } + } + return low; +} + +/** + * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` + * so only use this when actually needing stable sort. + */ +export function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; +} + +function _divideAndMerge(data: T[], compare: (a: T, b: T) => number): void { + if (data.length <= 1) { + // sorted + return; + } + const p = (data.length / 2) | 0; + const left = data.slice(0, p); + const right = data.slice(p); + + _divideAndMerge(left, compare); + _divideAndMerge(right, compare); + + let leftIdx = 0; + let rightIdx = 0; + let i = 0; + while (leftIdx < left.length && rightIdx < right.length) { + const ret = compare(left[leftIdx], right[rightIdx]); + if (ret <= 0) { + // smaller_equal -> take left to preserve order + data[(i += 1)] = left[(leftIdx += 1)]; + } else { + // greater -> take right + data[(i += 1)] = right[(rightIdx += 1)]; + } + } + while (leftIdx < left.length) { + data[(i += 1)] = left[(leftIdx += 1)]; + } + while (rightIdx < right.length) { + data[(i += 1)] = right[(rightIdx += 1)]; + } +} + +export function groupBy(data: T[], compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined; + + for (const element of mergeSort(data.slice(0), compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; +} + +type IMutableSplice = { + deleteCount: number; + start: number; + toInsert: T[]; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ISplice = Array & any; + +/** + * Diffs two *sorted* arrays and computes the splices which apply the diff. + */ +export function sortedDiff(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice[] { + const result: IMutableSplice[] = []; + + function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { + if (deleteCount === 0 && toInsert.length === 0) { + return; + } + + const latest = result[result.length - 1]; + + if (latest && latest.start + latest.deleteCount === start) { + latest.deleteCount += deleteCount; + latest.toInsert.push(...toInsert); + } else { + result.push({ start, deleteCount, toInsert }); + } + } + + let beforeIdx = 0; + let afterIdx = 0; + + while (beforeIdx !== before.length || afterIdx !== after.length) { + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + const n = compare(beforeElement, afterElement); + if (n === 0) { + // equal + beforeIdx += 1; + afterIdx += 1; + } else if (n < 0) { + // beforeElement is smaller -> before element removed + pushSplice(beforeIdx, 1, []); + beforeIdx += 1; + } else if (n > 0) { + // beforeElement is greater -> after element added + pushSplice(beforeIdx, 0, [afterElement]); + afterIdx += 1; + } + } + + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + } else if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); + } + + return result; +} + +/** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + */ +export function delta(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[]; added: T[] } { + const splices = sortedDiff(before, after, compare); + const removed: T[] = []; + const added: T[] = []; + + for (const splice of splices) { + removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); + added.push(...splice.toInsert); + } + + return { removed, added }; +} + +/** + * Returns the top N elements from the array. + * + * Faster than sorting the entire array when the array is a lot larger than N. + * + * @param array The unsorted array. + * @param compare A sort function for the elements. + * @param n The number of elements to return. + * @return The first n elemnts from array when sorted with compare. + */ +export function top(array: T[], compare: (a: T, b: T) => number, n: number): T[] { + if (n === 0) { + return []; + } + const result = array.slice(0, n).sort(compare); + topStep(array, compare, result, n, array.length); + return result; +} + +function topStep(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { + for (const n = result.length; i < m; i += 1) { + const element = array[i]; + if (compare(element, result[n - 1]) < 0) { + result.pop(); + const j = findFirst(result, (e) => compare(element, e) < 0); + result.splice(j, 0, element); + } + } +} + +/** + * @returns a new array with all undefined or null values removed. The original array is not modified at all. + */ +export function coalesce(array: T[]): T[] { + if (!array) { + return array; + } + + return array.filter((e) => !!e); +} + +/** + * Moves the element in the array for the provided positions. + */ +export function move(array: unknown[], from: number, to: number): void { + array.splice(to, 0, array.splice(from, 1)[0]); +} + +/** + * @returns {{false}} if the provided object is an array + * and not empty. + */ +export function isFalsyOrEmpty(obj: unknown): boolean { + return !Array.isArray(obj) || (>obj).length === 0; +} + +/** + * Removes duplicates from the given array. The optional keyFn allows to specify + * how elements are checked for equalness by returning a unique string for each. + */ +export function distinct(array: T[], keyFn?: (t: T) => string): T[] { + if (!keyFn) { + return array.filter((element, position) => array.indexOf(element) === position); + } + + const seen: Record = Object.create(null); + return array.filter((elem) => { + const key = keyFn(elem); + if (seen[key]) { + return false; + } + + seen[key] = true; + + return true; + }); +} + +export function uniqueFilter(keyFn: (t: T) => string): (t: T) => boolean { + const seen: Record = Object.create(null); + + return (element) => { + const key = keyFn(element); + + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }; +} + +export function firstIndex(array: T[], fn: (item: T) => boolean): number { + for (let i = 0; i < array.length; i += 1) { + const element = array[i]; + + if (fn(element)) { + return i; + } + } + + return -1; +} + +export function first(array: T[], fn: (item: T) => boolean, notFoundValue: T | null = null): T { + const idx = firstIndex(array, fn); + return idx < 0 && notFoundValue !== null ? notFoundValue : array[idx]; +} + +export function commonPrefixLength(one: T[], other: T[], eqls: (a: T, b: T) => boolean = (a, b) => a === b): number { + let result = 0; + + for (let i = 0, len = Math.min(one.length, other.length); i < len && eqls(one[i], other[i]); i += 1) { + result += 1; + } + + return result; +} + +export function flatten(arr: T[][]): T[] { + return ([] as T[]).concat(...arr); +} + +export function range(to: number): number[]; +export function range(from: number, to: number): number[]; +export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; + + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; + } + + const result: number[] = []; + + if (from <= to) { + for (let i = from; i < to; i += 1) { + result.push(i); + } + } else { + for (let i = from; i > to; i -= 1) { + result.push(i); + } + } + + return result; +} + +export function fill(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i += 1) { + arr[i] = valueFn(); + } + + return arr; +} + +export function index(array: T[], indexer: (t: T) => string): Record; +export function index(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): Record; +export function index( + array: T[], + indexer: (t: T) => string, + merger: (t: T, r: R) => R = (t) => t as unknown as R, +): Record { + return array.reduce((r, t) => { + const key = indexer(t); + r[key] = merger(t, r[key]); + return r; + }, Object.create(null)); +} + +/** + * Inserts an element into an array. Returns a function which, when + * called, will remove that element from the array. + */ +export function insert(array: T[], element: T): () => void { + array.push(element); + + return () => { + const idx = array.indexOf(element); + if (idx > -1) { + array.splice(idx, 1); + } + }; +} + +/** + * Insert `insertArr` inside `target` at `insertIndex`. + * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array + */ +export function arrayInsert(target: T[], insertIndex: number, insertArr: T[]): T[] { + const before = target.slice(0, insertIndex); + const after = target.slice(insertIndex); + return before.concat(insertArr, after); +} diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts new file mode 100644 index 0000000..6d032c4 --- /dev/null +++ b/src/test/mocks/vsc/charCode.ts @@ -0,0 +1,424 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = Caret, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = BackTick, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, +} diff --git a/src/test/mocks/vsc/copilotTools.ts b/src/test/mocks/vsc/copilotTools.ts new file mode 100644 index 0000000..7ec49d4 --- /dev/null +++ b/src/test/mocks/vsc/copilotTools.ts @@ -0,0 +1,56 @@ +/** + * A language model response part containing a piece of text, returned from a {@link LanguageModelChatResponse}. + */ +export class LanguageModelTextPart { + /** + * The text content of the part. + */ + value: string; + + /** + * Construct a text part with the given content. + * @param value The text content of the part. + */ + constructor(value: string) { + this.value = value; + } +} + +/** + * A result returned from a tool invocation. If using `@vscode/prompt-tsx`, this result may be rendered using a `ToolResult`. + */ +export class LanguageModelToolResult { + /** + * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in + * the future. + * @see {@link lm.invokeTool}. + */ + content: Array; + + /** + * Create a LanguageModelToolResult + * @param content A list of tool result content parts + */ + constructor(content: Array) { + this.content = content; + } +} + +/** + * A language model response part containing a PromptElementJSON from `@vscode/prompt-tsx`. + * @see {@link LanguageModelToolResult} + */ +export class LanguageModelPromptTsxPart { + /** + * The value of the part. + */ + value: unknown; + + /** + * Construct a prompt-tsx part with the given content. + * @param value The value of the part, the result of `renderPromptElementJSON` from `@vscode/prompt-tsx`. + */ + constructor(value: unknown) { + this.value = value; + } +} diff --git a/src/test/mocks/vsc/extHostedTypes.ts b/src/test/mocks/vsc/extHostedTypes.ts new file mode 100644 index 0000000..d80b45d --- /dev/null +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -0,0 +1,2320 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { relative } from 'path'; +import * as vscode from 'vscode'; +import * as vscMockHtmlContent from './htmlContent'; +import * as vscMockStrings from './strings'; +import * as vscUri from './uri'; +import { generateUuid } from './uuid'; + +export enum NotebookCellKind { + Markup = 1, + Code = 2, +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3, +} +export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4, +} + +export interface IRelativePattern { + base: string; + pattern: string; + pathToRelative(from: string, to: string): string; +} + +const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); + +export class Disposable { + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + + disposables = []; + } + }); + } + + private _callOnDispose: (() => void) | undefined; + + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } + + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; + } + } +} + +export class Position { + static Min(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isBefore(result)) { + result = p; + } + } + return result || new Position(0, 0); + } + + static Max(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isAfter(result)) { + result = p; + } + } + return result || new Position(0, 0); + } + + static isPosition(other: unknown): other is Position { + if (!other) { + return false; + } + if (other instanceof Position) { + return true; + } + const { line, character } = other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } + + private _line: number; + + private _character: number; + + get line(): number { + return this._line; + } + + get character(): number { + return this._character; + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative'); + } + if (character < 0) { + throw illegalArgument('character must be non-negative'); + } + this._line = line; + this._character = character; + } + + isBefore(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character < other._character; + } + + isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true; + } + if (other._line < this._line) { + return false; + } + return this._character <= other._character; + } + + isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other); + } + + isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other); + } + + isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character; + } + + compareTo(other: Position): number { + if (this._line < other._line) { + return -1; + } + if (this._line > other.line) { + return 1; + } + // equal line + if (this._character < other._character) { + return -1; + } + if (this._character > other._character) { + return 1; + } + // equal line and character + return 0; + } + + translate(change: { lineDelta?: number; characterDelta?: number }): Position; + + translate(lineDelta?: number, characterDelta?: number): Position; + + translate( + lineDeltaOrChange: number | { lineDelta?: number; characterDelta?: number } | undefined, + characterDelta = 0, + ): Position { + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument(); + } + + let lineDelta: number; + if (typeof lineDeltaOrChange === 'undefined') { + lineDelta = 0; + } else if (typeof lineDeltaOrChange === 'number') { + lineDelta = lineDeltaOrChange; + } else { + lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0; + characterDelta = + typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0; + } + + if (lineDelta === 0 && characterDelta === 0) { + return this; + } + return new Position(this.line + lineDelta, this.character + characterDelta); + } + + with(change: { line?: number; character?: number }): Position; + + with(line?: number, character?: number): Position; + + with( + lineOrChange: number | { line?: number; character?: number } | undefined, + character: number = this.character, + ): Position { + if (lineOrChange === null || character === null) { + throw illegalArgument(); + } + + let line: number; + if (typeof lineOrChange === 'undefined') { + line = this.line; + } else if (typeof lineOrChange === 'number') { + line = lineOrChange; + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; + } + + if (line === this.line && character === this.character) { + return this; + } + return new Position(line, character); + } + + toJSON(): { line: number; character: number } { + return { line: this.line, character: this.character }; + } +} + +export class Range { + static isRange(thing: unknown): thing is vscode.Range { + if (thing instanceof Range) { + return true; + } + if (!thing) { + return false; + } + return Position.isPosition((thing as Range).start) && Position.isPosition((thing as Range).end); + } + + protected _start: Position; + + protected _end: Position; + + get start(): Position { + return this._start; + } + + get end(): Position { + return this._end; + } + + constructor(start: Position, end: Position); + + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); + + constructor( + startLineOrStart: number | Position, + startColumnOrEnd: number | Position, + endLine?: number, + endColumn?: number, + ) { + let start: Position | undefined; + let end: Position | undefined; + + if ( + typeof startLineOrStart === 'number' && + typeof startColumnOrEnd === 'number' && + typeof endLine === 'number' && + typeof endColumn === 'number' + ) { + start = new Position(startLineOrStart, startColumnOrEnd); + end = new Position(endLine, endColumn); + } else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) { + start = startLineOrStart; + end = startColumnOrEnd; + } + + if (!start || !end) { + throw new Error('Invalid arguments'); + } + + if (start.isBefore(end)) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; + } + } + + contains(positionOrRange: Position | Range): boolean { + if (positionOrRange instanceof Range) { + return this.contains(positionOrRange._start) && this.contains(positionOrRange._end); + } + if (positionOrRange instanceof Position) { + if (positionOrRange.isBefore(this._start)) { + return false; + } + if (this._end.isBefore(positionOrRange)) { + return false; + } + return true; + } + return false; + } + + isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end); + } + + intersection(other: Range): Range | undefined { + const start = Position.Max(other.start, this._start); + const end = Position.Min(other.end, this._end); + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined; + } + return new Range(start, end); + } + + union(other: Range): Range { + if (this.contains(other)) { + return this; + } + if (other.contains(this)) { + return other; + } + const start = Position.Min(other.start, this._start); + const end = Position.Max(other.end, this.end); + return new Range(start, end); + } + + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } + + get isSingleLine(): boolean { + return this._start.line === this._end.line; + } + + with(change: { start?: Position; end?: Position }): Range; + + with(start?: Position, end?: Position): Range; + + with(startOrChange: Position | { start?: Position; end?: Position } | undefined, end: Position = this.end): Range { + if (startOrChange === null || end === null) { + throw illegalArgument(); + } + + let start: Position; + if (!startOrChange) { + start = this.start; + } else if (Position.isPosition(startOrChange)) { + start = startOrChange; + } else { + start = startOrChange.start || this.start; + end = startOrChange.end || this.end; + } + + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this; + } + return new Range(start, end); + } + + toJSON(): [Position, Position] { + return [this.start, this.end]; + } +} + +export class Selection extends Range { + static isSelection(thing: unknown): thing is Selection { + if (thing instanceof Selection) { + return true; + } + if (!thing) { + return false; + } + return ( + Range.isRange(thing) && + Position.isPosition((thing).anchor) && + Position.isPosition((thing).active) && + typeof (thing).isReversed === 'boolean' + ); + } + + private _anchor: Position; + + public get anchor(): Position { + return this._anchor; + } + + private _active: Position; + + public get active(): Position { + return this._active; + } + + constructor(anchor: Position, active: Position); + + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number); + + constructor( + anchorLineOrAnchor: number | Position, + anchorColumnOrActive: number | Position, + activeLine?: number, + activeColumn?: number, + ) { + let anchor: Position | undefined; + let active: Position | undefined; + + if ( + typeof anchorLineOrAnchor === 'number' && + typeof anchorColumnOrActive === 'number' && + typeof activeLine === 'number' && + typeof activeColumn === 'number' + ) { + anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive); + active = new Position(activeLine, activeColumn); + } else if (anchorLineOrAnchor instanceof Position && anchorColumnOrActive instanceof Position) { + anchor = anchorLineOrAnchor; + active = anchorColumnOrActive; + } + + if (!anchor || !active) { + throw new Error('Invalid arguments'); + } + + super(anchor, active); + + this._anchor = anchor; + this._active = active; + } + + get isReversed(): boolean { + return this._anchor === this._end; + } + + toJSON(): [Position, Position] { + return { + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor, + } as unknown as [Position, Position]; + } +} + +export enum EndOfLine { + LF = 1, + CRLF = 2, +} + +export class TextEdit { + static isTextEdit(thing: unknown): thing is TextEdit { + if (thing instanceof TextEdit) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange(thing) && typeof (thing).newText === 'string'; + } + + static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText); + } + + static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText); + } + + static delete(range: Range): TextEdit { + return TextEdit.replace(range, ''); + } + + static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + ret.newEol = eol; + return ret; + } + + _range: Range = new Range(new Position(0, 0), new Position(0, 0)); + + newText = ''; + + _newEol: EndOfLine = EndOfLine.LF; + + get range(): Range { + return this._range; + } + + set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range'); + } + this._range = value; + } + + get newEol(): EndOfLine { + return this._newEol; + } + + set newEol(value: EndOfLine) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol'); + } + this._newEol = value; + } + + constructor(range: Range, newText: string) { + this.range = range; + this.newText = newText; + } +} + +export class WorkspaceEdit implements vscode.WorkspaceEdit { + appendNotebookCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.NotebookCellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } + + replaceNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } + + appendNotebookCellOutputItems( + _uri: vscode.Uri, + _index: number, + _outputId: string, + _items: vscode.NotebookCellOutputItem[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } + + replaceNotebookCells( + _uri: vscode.Uri, + _start: number, + _end: number, + _cells: vscode.NotebookCellData[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } + + replaceNotebookCellOutput( + _uri: vscode.Uri, + _index: number, + _outputs: vscode.NotebookCellOutput[], + _metadata?: vscode.WorkspaceEditEntryMetadata, + ): void { + // Noop. + } + + private _seqPool = 0; + + private _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.URI }[] = []; + + private _textEdits = new Map(); + + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } + + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } + + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool+= 1, from, to }); + // } + + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } + + createFile(_uri: vscode.Uri, _options?: { overwrite?: boolean; ignoreIfExists?: boolean }): void { + throw new Error('Method not implemented.'); + } + + deleteFile(_uri: vscode.Uri, _options?: { recursive?: boolean; ignoreIfNotExists?: boolean }): void { + throw new Error('Method not implemented.'); + } + + renameFile( + _oldUri: vscode.Uri, + _newUri: vscode.Uri, + _options?: { overwrite?: boolean; ignoreIfExists?: boolean }, + ): void { + throw new Error('Method not implemented.'); + } + + replace(uri: vscUri.URI, range: Range, newText: string): void { + const edit = new TextEdit(range, newText); + let array = this.get(uri); + if (array) { + array.push(edit); + } else { + array = [edit]; + } + this.set(uri, array); + } + + insert(resource: vscUri.URI, position: Position, newText: string): void { + this.replace(resource, new Range(position, position), newText); + } + + delete(resource: vscUri.URI, range: Range): void { + this.replace(resource, range, ''); + } + + has(uri: vscUri.URI): boolean { + return this._textEdits.has(uri.toString()); + } + + set(uri: vscUri.URI, edits: readonly unknown[]): void { + let data = this._textEdits.get(uri.toString()); + if (!data) { + data = { seq: (this._seqPool += 1), uri, edits: [] }; + this._textEdits.set(uri.toString(), data); + } + if (!edits) { + data.edits = []; + } else { + data.edits = edits.slice(0) as TextEdit[]; + } + } + + get(uri: vscUri.URI): TextEdit[] { + if (!this._textEdits.has(uri.toString())) { + return []; + } + const { edits } = this._textEdits.get(uri.toString()) || {}; + return edits ? edits.slice() : []; + } + + entries(): [vscUri.URI, TextEdit[]][] { + const res: [vscUri.URI, TextEdit[]][] = []; + this._textEdits.forEach((value) => res.push([value.uri, value.edits])); + return res.slice(); + } + + allEntries(): ([vscUri.URI, TextEdit[]] | [vscUri.URI, vscUri.URI])[] { + return this.entries(); + // // use the 'seq' the we have assigned when inserting + // // the operation and use that order in the resulting + // // array + // const res: ([vscUri.URI, TextEdit[]] | [vscUri.URI,vscUri.URI])[] = []; + // this._textEdits.forEach(value => { + // const { seq, uri, edits } = value; + // res[seq] = [uri, edits]; + // }); + // this._resourceEdits.forEach(value => { + // const { seq, from, to } = value; + // res[seq] = [from, to]; + // }); + // return res; + } + + get size(): number { + return this._textEdits.size + this._resourceEdits.length; + } + + toJSON(): [vscUri.URI, TextEdit[]][] { + return this.entries(); + } +} + +export class SnippetString { + static isSnippetString(thing: unknown): thing is SnippetString { + if (thing instanceof SnippetString) { + return true; + } + if (!thing) { + return false; + } + return typeof (thing).value === 'string'; + } + + private static _escape(value: string): string { + return value.replace(/\$|}|\\/g, '\\$&'); + } + + private _tabstop = 1; + + value: string; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(string: string): SnippetString { + this.value += SnippetString._escape(string); + return this; + } + + appendTabstop(number: number = (this._tabstop += 1)): SnippetString { + this.value += '$'; + this.value += number; + return this; + } + + appendPlaceholder( + value: string | ((snippet: SnippetString) => void), + number: number = (this._tabstop += 1), + ): SnippetString { + if (typeof value === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + value(nested); + this._tabstop = nested._tabstop; + value = nested.value; + } else { + value = SnippetString._escape(value); + } + + this.value += '${'; + this.value += number; + this.value += ':'; + this.value += value; + this.value += '}'; + + return this; + } + + appendChoice(values: string[], number: number = (this._tabstop += 1)): SnippetString { + const value = SnippetString._escape(values.toString()); + + this.value += '${'; + this.value += number; + this.value += '|'; + this.value += value; + this.value += '|}'; + + return this; + } + + appendVariable(name: string, defaultValue?: string | ((snippet: SnippetString) => void)): SnippetString { + if (typeof defaultValue === 'function') { + const nested = new SnippetString(); + nested._tabstop = this._tabstop; + defaultValue(nested); + this._tabstop = nested._tabstop; + defaultValue = nested.value; + } else if (typeof defaultValue === 'string') { + defaultValue = defaultValue.replace(/\$|}/g, '\\$&'); // CodeQL [SM02383] don't escape backslashes here (by design) + } + + this.value += '${'; + this.value += name; + if (defaultValue) { + this.value += ':'; + this.value += defaultValue; + } + this.value += '}'; + + return this; + } +} + +export enum DiagnosticTag { + Unnecessary = 1, +} + +export enum DiagnosticSeverity { + Hint = 3, + Information = 2, + Warning = 1, + Error = 0, +} + +export class Location { + static isLocation(thing: unknown): thing is Location { + if (thing instanceof Location) { + return true; + } + if (!thing) { + return false; + } + return Range.isRange((thing).range) && vscUri.URI.isUri((thing).uri); + } + + uri: vscUri.URI; + + range: Range = new Range(new Position(0, 0), new Position(0, 0)); + + constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { + this.uri = uri; + + if (!rangeOrPosition) { + // that's OK + } else if (rangeOrPosition instanceof Range) { + this.range = rangeOrPosition; + } else if (rangeOrPosition instanceof Position) { + this.range = new Range(rangeOrPosition, rangeOrPosition); + } else { + throw new Error('Illegal argument'); + } + } + + toJSON(): { uri: vscUri.URI; range: Range } { + return { + uri: this.uri, + range: this.range, + }; + } +} + +export class DiagnosticRelatedInformation { + static is(thing: unknown): thing is DiagnosticRelatedInformation { + if (!thing) { + return false; + } + return ( + typeof (thing).message === 'string' && + (thing).location && + Range.isRange((thing).location.range) && + vscUri.URI.isUri((thing).location.uri) + ); + } + + location: Location; + + message: string; + + constructor(location: Location, message: string) { + this.location = location; + this.message = message; + } +} + +export class Diagnostic { + range: Range; + + message: string; + + source = ''; + + code: string | number = ''; + + severity: DiagnosticSeverity; + + relatedInformation: DiagnosticRelatedInformation[] = []; + + customTags?: DiagnosticTag[]; + + constructor(range: Range, message: string, severity: DiagnosticSeverity = DiagnosticSeverity.Error) { + this.range = range; + this.message = message; + this.severity = severity; + } + + toJSON(): { severity: DiagnosticSeverity; message: string; range: Range; source: string; code: string | number } { + return { + severity: DiagnosticSeverity[this.severity] as unknown as DiagnosticSeverity, + message: this.message, + range: this.range, + source: this.source, + code: this.code, + }; + } +} + +export class Hover { + public contents: vscode.MarkdownString[]; + + public range: Range; + + constructor(contents: vscode.MarkdownString | vscode.MarkdownString[], range?: Range) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); + } + if (Array.isArray(contents)) { + this.contents = contents; + } else if (vscMockHtmlContent.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + + this.range = range || new Range(new Position(0, 0), new Position(0, 0)); + } +} + +export enum DocumentHighlightKind { + Text = 0, + Read = 1, + Write = 2, +} + +export class DocumentHighlight { + range: Range; + + kind: DocumentHighlightKind; + + constructor(range: Range, kind: DocumentHighlightKind = DocumentHighlightKind.Text) { + this.range = range; + this.kind = kind; + } + + toJSON(): { range: Range; kind: DocumentHighlightKind } { + return { + range: this.range, + kind: DocumentHighlightKind[this.kind] as unknown as DocumentHighlightKind, + }; + } +} + +export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} + +export class SymbolInformation { + name: string; + + location: Location = new Location( + vscUri.URI.parse('testLocation'), + new Range(new Position(0, 0), new Position(0, 0)), + ); + + kind: SymbolKind; + + containerName: string; + + constructor(name: string, kind: SymbolKind, containerName: string, location: Location); + + constructor(name: string, kind: SymbolKind, range: Range, uri?: vscUri.URI, containerName?: string); + + constructor( + name: string, + kind: SymbolKind, + rangeOrContainer: string | Range, + locationOrUri?: Location | vscUri.URI, + containerName?: string, + ) { + this.name = name; + this.kind = kind; + this.containerName = containerName || ''; + + if (typeof rangeOrContainer === 'string') { + this.containerName = rangeOrContainer; + } + + if (locationOrUri instanceof Location) { + this.location = locationOrUri; + } else if (rangeOrContainer instanceof Range) { + this.location = new Location(locationOrUri as vscUri.URI, rangeOrContainer); + } + } + + toJSON(): { name: string; kind: SymbolKind; location: Location; containerName: string } { + return { + name: this.name, + kind: SymbolKind[this.kind] as unknown as SymbolKind, + location: this.location, + containerName: this.containerName, + }; + } +} + +export class SymbolInformation2 extends SymbolInformation { + definingRange: Range; + + children: SymbolInformation2[]; + + constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { + super(name, kind, containerName, location); + + this.children = []; + this.definingRange = location.range; + } +} + +export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, +} + +export class CodeAction { + title: string; + + command?: vscode.Command; + + edit?: WorkspaceEdit; + + dianostics?: Diagnostic[]; + + kind?: CodeActionKind; + + constructor(title: string, kind?: CodeActionKind) { + this.title = title; + this.kind = kind; + } +} + +export class CodeActionKind { + private static readonly sep = '.'; + + public static readonly Empty = new CodeActionKind(''); + + public static readonly QuickFix = CodeActionKind.Empty.append('quickfix'); + + public static readonly Refactor = CodeActionKind.Empty.append('refactor'); + + public static readonly RefactorExtract = CodeActionKind.Refactor.append('extract'); + + public static readonly RefactorInline = CodeActionKind.Refactor.append('inline'); + + public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite'); + + public static readonly Source = CodeActionKind.Empty.append('source'); + + public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports'); + + constructor(public readonly value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); + } + + public contains(other: CodeActionKind): boolean { + return this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep); + } +} + +export class CodeLens { + range: Range; + + command: vscode.Command | undefined; + + constructor(range: Range, command?: vscode.Command) { + this.range = range; + this.command = command; + } + + get isResolved(): boolean { + return !!this.command; + } +} + +export class MarkdownString { + value: string; + + isTrusted?: boolean; + + constructor(value?: string) { + this.value = value || ''; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(code: string, language = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } +} + +export class ParameterInformation { + label: string; + + documentation?: string | MarkdownString; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + } +} + +export class SignatureInformation { + label: string; + + documentation?: string | MarkdownString; + + parameters: ParameterInformation[]; + + constructor(label: string, documentation?: string | MarkdownString) { + this.label = label; + this.documentation = documentation; + this.parameters = []; + } +} + +export class SignatureHelp { + signatures: SignatureInformation[]; + + activeSignature: number; + + activeParameter: number; + + constructor() { + this.signatures = []; + this.activeSignature = -1; + this.activeParameter = -1; + } +} + +export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2, +} + +export interface CompletionContext { + triggerKind: CompletionTriggerKind; + triggerCharacter: string; +} + +export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + File = 16, + Reference = 17, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26, +} + +export enum CompletionItemTag { + Deprecated = 1, +} + +export interface CompletionItemLabel { + name: string; + signature?: string; + qualifier?: string; + type?: string; +} + +export class CompletionItem { + label: string; + + label2?: CompletionItemLabel; + + kind?: CompletionItemKind; + + tags?: CompletionItemTag[]; + + detail?: string; + + documentation?: string | MarkdownString; + + sortText?: string; + + filterText?: string; + + preselect?: boolean; + + insertText?: string | SnippetString; + + keepWhitespace?: boolean; + + range?: Range; + + commitCharacters?: string[]; + + textEdit?: TextEdit; + + additionalTextEdits?: TextEdit[]; + + command?: vscode.Command; + + constructor(label: string, kind?: CompletionItemKind) { + this.label = label; + this.kind = kind; + } + + toJSON(): { + label: string; + label2?: CompletionItemLabel; + kind?: CompletionItemKind; + detail?: string; + documentation?: string | MarkdownString; + sortText?: string; + filterText?: string; + preselect?: boolean; + insertText?: string | SnippetString; + textEdit?: TextEdit; + } { + return { + label: this.label, + label2: this.label2, + kind: this.kind && (CompletionItemKind[this.kind] as unknown as CompletionItemKind), + detail: this.detail, + documentation: this.documentation, + sortText: this.sortText, + filterText: this.filterText, + preselect: this.preselect, + insertText: this.insertText, + textEdit: this.textEdit, + }; + } +} + +export class CompletionList { + isIncomplete?: boolean; + + items: vscode.CompletionItem[]; + + constructor(items: vscode.CompletionItem[] = [], isIncomplete = false) { + this.items = items; + this.isIncomplete = isIncomplete; + } +} + +export class CallHierarchyItem { + name: string; + + kind: SymbolKind; + + tags?: ReadonlyArray; + + detail?: string; + + uri: vscode.Uri; + + range: vscode.Range; + + selectionRange: vscode.Range; + + constructor( + kind: vscode.SymbolKind, + name: string, + detail: string, + uri: vscode.Uri, + range: vscode.Range, + selectionRange: vscode.Range, + ) { + this.kind = kind; + this.name = name; + this.detail = detail; + this.uri = uri; + this.range = range; + this.selectionRange = selectionRange; + } +} + +export enum ViewColumn { + Active = -1, + Beside = -2, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6, + Seven = 7, + Eight = 8, + Nine = 9, +} + +export enum StatusBarAlignment { + Left = 1, + Right = 2, +} + +export enum TextEditorLineNumbersStyle { + Off = 0, + On = 1, + Relative = 2, +} + +export enum TextDocumentSaveReason { + Manual = 1, + AfterDelay = 2, + FocusOut = 3, +} + +export enum TextEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, + AtTop = 3, +} + +export enum TextEditorSelectionChangeKind { + Keyboard = 1, + Mouse = 2, + Command = 3, +} + +/** + * These values match very carefully the values of `TrackedRangeStickiness` + */ +export enum DecorationRangeBehavior { + /** + * TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges + */ + OpenOpen = 0, + /** + * TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + */ + ClosedClosed = 1, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingBefore + */ + OpenClosed = 2, + /** + * TrackedRangeStickiness.GrowsOnlyWhenTypingAfter + */ + ClosedOpen = 3, +} + +export namespace TextEditorSelectionChangeKind { + export function fromValue(s: string): TextEditorSelectionChangeKind | undefined { + switch (s) { + case 'keyboard': + return TextEditorSelectionChangeKind.Keyboard; + case 'mouse': + return TextEditorSelectionChangeKind.Mouse; + case 'api': + return TextEditorSelectionChangeKind.Command; + default: + return undefined; + } + } +} + +export class DocumentLink { + range: Range; + + target: vscUri.URI; + + constructor(range: Range, target: vscUri.URI) { + if (target && !(target instanceof vscUri.URI)) { + throw illegalArgument('target'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.target = target; + } +} + +export class Color { + readonly red: number; + + readonly green: number; + + readonly blue: number; + + readonly alpha: number; + + constructor(red: number, green: number, blue: number, alpha: number) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } +} + +export type IColorFormat = string | { opaque: string; transparent: string }; + +export class ColorInformation { + range: Range; + + color: Color; + + constructor(range: Range, color: Color) { + if (color && !(color instanceof Color)) { + throw illegalArgument('color'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); + } + this.range = range; + this.color = color; + } +} + +export class ColorPresentation { + label: string; + + textEdit?: TextEdit; + + additionalTextEdits?: TextEdit[]; + + constructor(label: string) { + if (!label || typeof label !== 'string') { + throw illegalArgument('label'); + } + this.label = label; + } +} + +export enum ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2, +} + +export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2, +} + +export enum TaskRevealKind { + Always = 1, + + Silent = 2, + + Never = 3, +} + +export enum TaskPanelKind { + Shared = 1, + + Dedicated = 2, + + New = 3, +} + +export class TaskGroup implements vscode.TaskGroup { + private _id: string; + + public isDefault = undefined; + + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); + + public static Build: TaskGroup = new TaskGroup('build', 'Build'); + + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); + + public static Test: TaskGroup = new TaskGroup('test', 'Test'); + + public static from(value: string): TaskGroup | undefined { + switch (value) { + case 'clean': + return TaskGroup.Clean; + case 'build': + return TaskGroup.Build; + case 'rebuild': + return TaskGroup.Rebuild; + case 'test': + return TaskGroup.Test; + default: + return undefined; + } + } + + constructor(id: string, _label: string) { + if (typeof id !== 'string') { + throw illegalArgument('name'); + } + if (typeof _label !== 'string') { + throw illegalArgument('name'); + } + this._id = id; + } + + get id(): string { + return this._id; + } +} + +export class ProcessExecution implements vscode.ProcessExecution { + private _process: string; + + private _args: string[] | undefined; + + private _options: vscode.ProcessExecutionOptions | undefined; + + constructor(process: string, options?: vscode.ProcessExecutionOptions); + + constructor(process: string, args: string[], options?: vscode.ProcessExecutionOptions); + + constructor( + process: string, + varg1?: string[] | vscode.ProcessExecutionOptions, + varg2?: vscode.ProcessExecutionOptions, + ) { + if (typeof process !== 'string') { + throw illegalArgument('process'); + } + this._process = process; + if (varg1) { + if (Array.isArray(varg1)) { + this._args = varg1; + this._options = varg2; + } else { + this._options = varg1; + } + } + + if (this._args === undefined) { + this._args = []; + } + } + + get process(): string { + return this._process; + } + + set process(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('process'); + } + this._process = value; + } + + get args(): string[] { + return this._args || []; + } + + set args(value: string[]) { + if (!Array.isArray(value)) { + value = []; + } + this._args = value; + } + + get options(): vscode.ProcessExecutionOptions { + return this._options || {}; + } + + set options(value: vscode.ProcessExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('process'); + // if (this._process !== void 0) { + // hash.update(this._process); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(arg); + // } + // } + // return hash.digest('hex'); + throw new Error('Not supported'); + } +} + +export class ShellExecution implements vscode.ShellExecution { + private _commandLine = ''; + + private _command: string | vscode.ShellQuotedString = ''; + + private _args: (string | vscode.ShellQuotedString)[] = []; + + private _options: vscode.ShellExecutionOptions | undefined; + + constructor(commandLine: string, options?: vscode.ShellExecutionOptions); + + constructor( + command: string | vscode.ShellQuotedString, + args: (string | vscode.ShellQuotedString)[], + options?: vscode.ShellExecutionOptions, + ); + + constructor( + arg0: string | vscode.ShellQuotedString, + arg1?: vscode.ShellExecutionOptions | (string | vscode.ShellQuotedString)[], + arg2?: vscode.ShellExecutionOptions, + ) { + if (Array.isArray(arg1)) { + if (!arg0) { + throw illegalArgument("command can't be undefined or null"); + } + if (typeof arg0 !== 'string' && typeof arg0.value !== 'string') { + throw illegalArgument('command'); + } + this._command = arg0; + this._args = arg1 as (string | vscode.ShellQuotedString)[]; + this._options = arg2; + } else { + if (typeof arg0 !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = arg0; + this._options = arg1; + } + } + + get commandLine(): string { + return this._commandLine; + } + + set commandLine(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('commandLine'); + } + this._commandLine = value; + } + + get command(): string | vscode.ShellQuotedString { + return this._command; + } + + set command(value: string | vscode.ShellQuotedString) { + if (typeof value !== 'string' && typeof value.value !== 'string') { + throw illegalArgument('command'); + } + this._command = value; + } + + get args(): (string | vscode.ShellQuotedString)[] { + return this._args; + } + + set args(value: (string | vscode.ShellQuotedString)[]) { + this._args = value || []; + } + + get options(): vscode.ShellExecutionOptions { + return this._options || {}; + } + + set options(value: vscode.ShellExecutionOptions) { + this._options = value; + } + + public computeId(): string { + // const hash = crypto.createHash('md5'); + // hash.update('shell'); + // if (this._commandLine !== void 0) { + // hash.update(this._commandLine); + // } + // if (this._command !== void 0) { + // hash.update(typeof this._command === 'string' ? this._command : this._command.value); + // } + // if (this._args && this._args.length > 0) { + // for (let arg of this._args) { + // hash.update(typeof arg === 'string' ? arg : arg.value); + // } + // } + // return hash.digest('hex'); + throw new Error('Not spported'); + } +} + +export enum ShellQuoting { + Escape = 1, + Strong = 2, + Weak = 3, +} + +export enum TaskScope { + Global = 1, + Workspace = 2, +} + +export class Task implements vscode.Task { + private static ProcessType = 'process'; + + private static ShellType = 'shell'; + + private static EmptyType = '$empty'; + + private __id: string | undefined; + + private _definition!: vscode.TaskDefinition; + + private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; + + private _name!: string; + + private _execution: ProcessExecution | ShellExecution | undefined; + + private _problemMatchers: string[]; + + private _hasDefinedMatchers: boolean; + + private _isBackground: boolean; + + private _source!: string; + + private _group: TaskGroup | undefined; + + private _presentationOptions: vscode.TaskPresentationOptions; + + private _runOptions: vscode.RunOptions; + + constructor( + definition: vscode.TaskDefinition, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); + + constructor( + definition: vscode.TaskDefinition, + scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, + name: string, + source: string, + execution?: ProcessExecution | ShellExecution, + problemMatchers?: string | string[], + ); + + constructor( + definition: vscode.TaskDefinition, + arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, + arg3: string, + arg4?: string | ProcessExecution | ShellExecution, + arg5?: ProcessExecution | ShellExecution | string | string[], + arg6?: string | string[], + ) { + this.definition = definition; + let problemMatchers: string | string[]; + if (typeof arg2 === 'string') { + this.name = arg2; + this.source = arg3; + this.execution = arg4 as ProcessExecution | ShellExecution; + problemMatchers = arg5 as string | string[]; + } else { + this.target = arg2; + this.name = arg3; + this.source = arg4 as string; + this.execution = arg5 as ProcessExecution | ShellExecution; + problemMatchers = arg6 as string | string[]; + } + if (typeof problemMatchers === 'string') { + this._problemMatchers = [problemMatchers]; + this._hasDefinedMatchers = true; + } else if (Array.isArray(problemMatchers)) { + this._problemMatchers = problemMatchers; + this._hasDefinedMatchers = true; + } else { + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } + this._isBackground = false; + this._presentationOptions = Object.create(null); + this._runOptions = Object.create(null); + } + + get _id(): string | undefined { + return this.__id; + } + + set _id(value: string | undefined) { + this.__id = value; + } + + private clear(): void { + if (this.__id === undefined) { + return; + } + this.__id = undefined; + this._scope = undefined; + this.computeDefinitionBasedOnExecution(); + } + + private computeDefinitionBasedOnExecution(): void { + if (this._execution instanceof ProcessExecution) { + this._definition = { + type: Task.ProcessType, + id: this._execution.computeId(), + }; + } else if (this._execution instanceof ShellExecution) { + this._definition = { + type: Task.ShellType, + id: this._execution.computeId(), + }; + } else { + this._definition = { + type: Task.EmptyType, + id: generateUuid(), + }; + } + } + + get definition(): vscode.TaskDefinition { + return this._definition; + } + + set definition(value: vscode.TaskDefinition) { + if (value === undefined || value === null) { + throw illegalArgument("Kind can't be undefined or null"); + } + this.clear(); + this._definition = value; + } + + get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined { + return this._scope; + } + + set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { + this.clear(); + this._scope = value; + } + + get name(): string { + return this._name; + } + + set name(value: string) { + if (typeof value !== 'string') { + throw illegalArgument('name'); + } + this.clear(); + this._name = value; + } + + get execution(): ProcessExecution | ShellExecution | undefined { + return this._execution; + } + + set execution(value: ProcessExecution | ShellExecution | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this._execution = value; + const { type } = this._definition; + if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { + this.computeDefinitionBasedOnExecution(); + } + } + + get problemMatchers(): string[] { + return this._problemMatchers; + } + + set problemMatchers(value: string[]) { + if (!Array.isArray(value)) { + this.clear(); + this._problemMatchers = []; + this._hasDefinedMatchers = false; + } else { + this.clear(); + this._problemMatchers = value; + this._hasDefinedMatchers = true; + } + } + + get hasDefinedMatchers(): boolean { + return this._hasDefinedMatchers; + } + + get isBackground(): boolean { + return this._isBackground; + } + + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; + } + this.clear(); + this._isBackground = value; + } + + get source(): string { + return this._source; + } + + set source(value: string) { + if (typeof value !== 'string' || value.length === 0) { + throw illegalArgument('source must be a string of length > 0'); + } + this.clear(); + this._source = value; + } + + get group(): TaskGroup | undefined { + return this._group; + } + + set group(value: TaskGroup | undefined) { + if (value === null) { + value = undefined; + } + this.clear(); + this._group = value; + } + + get presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } + + set presentationOptions(value: vscode.TaskPresentationOptions) { + if (value === null || value === undefined) { + value = Object.create(null); + } + this.clear(); + this._presentationOptions = value; + } + + get runOptions(): vscode.RunOptions { + return this._runOptions; + } + + set runOptions(value: vscode.RunOptions) { + if (value === null || value === undefined) { + value = Object.create(null); + } + this.clear(); + this._runOptions = value; + } +} + +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} + +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2, +} + +export class TreeItem { + label?: string; + id?: string; + description?: string | boolean; + + resourceUri?: vscUri.URI; + + iconPath?: string | vscode.IconPath; + command?: vscode.Command; + + contextValue?: string; + + tooltip?: string; + + checkboxState?: vscode.TreeItemCheckboxState; + + accessibilityInformation?: vscode.AccessibilityInformation; + + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState); + + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState); + + constructor( + arg1: string | vscUri.URI, + public collapsibleState: vscode.TreeItemCollapsibleState = TreeItemCollapsibleState.None, + ) { + if (arg1 instanceof vscUri.URI) { + this.resourceUri = arg1; + } else { + this.label = arg1; + } + } +} + +export class ThemeIcon { + static readonly File = new ThemeIcon('file'); + + static readonly Folder = new ThemeIcon('folder'); + + readonly id: string; + + private constructor(id: string) { + this.id = id; + } +} + +export class ThemeColor { + id: string; + + constructor(id: string) { + this.id = id; + } +} + +export enum ConfigurationTarget { + Global = 1, + + Workspace = 2, + + WorkspaceFolder = 3, +} + +export class RelativePattern implements IRelativePattern { + baseUri: vscode.Uri; + + base: string; + + pattern: string; + + constructor(base: vscode.WorkspaceFolder | string, pattern: string) { + if (typeof base !== 'string') { + if (!base || !vscUri.URI.isUri(base.uri)) { + throw illegalArgument('base'); + } + } + + if (typeof pattern !== 'string') { + throw illegalArgument('pattern'); + } + + this.baseUri = typeof base === 'string' ? vscUri.URI.parse(base) : base.uri; + this.base = typeof base === 'string' ? base : base.uri.fsPath; + this.pattern = pattern; + } + + public pathToRelative(from: string, to: string): string { + return relative(from, to); + } +} + +export class Breakpoint { + readonly enabled: boolean; + + readonly condition?: string; + + readonly hitCondition?: string; + + readonly logMessage?: string; + + protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + this.enabled = typeof enabled === 'boolean' ? enabled : true; + if (typeof condition === 'string') { + this.condition = condition; + } + if (typeof hitCondition === 'string') { + this.hitCondition = hitCondition; + } + if (typeof logMessage === 'string') { + this.logMessage = logMessage; + } + } +} + +export class SourceBreakpoint extends Breakpoint { + readonly location: Location; + + constructor(location: Location, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) { + super(enabled, condition, hitCondition, logMessage); + if (location === null) { + throw illegalArgument('location'); + } + this.location = location; + } +} + +export class FunctionBreakpoint extends Breakpoint { + readonly functionName: string; + + constructor( + functionName: string, + enabled?: boolean, + condition?: string, + hitCondition?: string, + logMessage?: string, + ) { + super(enabled, condition, hitCondition, logMessage); + if (!functionName) { + throw illegalArgument('functionName'); + } + this.functionName = functionName; + } +} + +export class DebugAdapterExecutable { + readonly command: string; + + readonly args: string[]; + + constructor(command: string, args?: string[]) { + this.command = command; + this.args = args || []; + } +} + +export class DebugAdapterServer { + readonly port: number; + + readonly host?: string; + + constructor(port: number, host?: string) { + this.port = port; + this.host = host; + } +} + +export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7, +} + +// #region file api + +export enum FileChangeType { + Changed = 1, + Created = 2, + Deleted = 3, +} + +export class FileSystemError extends Error { + static FileExists(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryExists', FileSystemError.FileExists); + } + + static FileNotFound(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotFound', FileSystemError.FileNotFound); + } + + static FileNotADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryNotADirectory', FileSystemError.FileNotADirectory); + } + + static FileIsADirectory(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'EntryIsADirectory', FileSystemError.FileIsADirectory); + } + + static NoPermissions(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'NoPermissions', FileSystemError.NoPermissions); + } + + static Unavailable(messageOrUri?: string | vscUri.URI): FileSystemError { + return new FileSystemError(messageOrUri, 'Unavailable', FileSystemError.Unavailable); + } + + constructor(uriOrMessage?: string | vscUri.URI, code?: string, terminator?: () => void) { + super(vscUri.URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.name = code ? `${code} (FileSystemError)` : `FileSystemError`; + + Object.setPrototypeOf(this, FileSystemError.prototype); + + if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { + // nice stack traces + Error.captureStackTrace(this, terminator); + } + } + + public get code(): string { + return ''; + } +} + +// #endregion + +// #region folding api + +export class FoldingRange { + start: number; + + end: number; + + kind?: FoldingRangeKind; + + constructor(start: number, end: number, kind?: FoldingRangeKind) { + this.start = start; + this.end = end; + this.kind = kind; + } +} + +export enum FoldingRangeKind { + Comment = 1, + Imports = 2, + Region = 3, +} + +// #endregion + +export enum CommentThreadCollapsibleState { + /** + * Determines an item is collapsed + */ + Collapsed = 0, + /** + * Determines an item is expanded + */ + Expanded = 1, +} + +export class QuickInputButtons { + static readonly Back: vscode.QuickInputButton = { iconPath: vscUri.URI.file('back') }; +} + +export enum SymbolTag { + Deprecated = 1, +} + +export class TypeHierarchyItem { + name: string; + + kind: SymbolKind; + + tags?: ReadonlyArray; + + detail?: string; + + uri: vscode.Uri; + + range: Range; + + selectionRange: Range; + + constructor(kind: SymbolKind, name: string, detail: string, uri: vscode.Uri, range: Range, selectionRange: Range) { + this.name = name; + this.kind = kind; + this.detail = detail; + this.uri = uri; + this.range = range; + this.selectionRange = selectionRange; + } +} + +export declare type LSPObject = { + [key: string]: LSPAny; +}; + +export declare type LSPArray = LSPAny[]; + +export declare type integer = number; +export declare type uinteger = number; +export declare type decimal = number; + +export declare type LSPAny = LSPObject | LSPArray | string | integer | uinteger | decimal | boolean | null; + +export class ProtocolTypeHierarchyItem extends TypeHierarchyItem { + data?; + + constructor( + kind: SymbolKind, + name: string, + detail: string, + uri: vscode.Uri, + range: Range, + selectionRange: Range, + data?: LSPAny, + ) { + super(kind, name, detail, uri, range, selectionRange); + this.data = data; + } +} + +export class CancellationError extends Error {} + +export class LSPCancellationError extends CancellationError { + data; + + constructor(data: any) { + super(); + this.data = data; + } +} diff --git a/src/test/mocks/vsc/htmlContent.ts b/src/test/mocks/vsc/htmlContent.ts new file mode 100644 index 0000000..0812c39 --- /dev/null +++ b/src/test/mocks/vsc/htmlContent.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscMockArrays from './arrays'; + +export interface IMarkdownString { + value: string; + isTrusted?: boolean; +} + +export class MarkdownString implements IMarkdownString { + value: string; + + isTrusted?: boolean; + + constructor(value = '') { + this.value = value; + } + + appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); + return this; + } + + appendMarkdown(value: string): MarkdownString { + this.value += value; + return this; + } + + appendCodeblock(langId: string, code: string): MarkdownString { + this.value += '\n```'; + this.value += langId; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } +} + +export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[]): boolean { + if (isMarkdownString(oneOrMany)) { + return !oneOrMany.value; + } + if (Array.isArray(oneOrMany)) { + return oneOrMany.every(isEmptyMarkdownString); + } + return true; +} + +export function isMarkdownString(thing: unknown): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; + } + if (thing && typeof thing === 'object') { + return ( + typeof (thing).value === 'string' && + (typeof (thing).isTrusted === 'boolean' || + (thing).isTrusted === undefined) + ); + } + return false; +} + +export function markedStringsEquals( + a: IMarkdownString | IMarkdownString[], + b: IMarkdownString | IMarkdownString[], +): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + if (Array.isArray(a) && Array.isArray(b)) { + return vscMockArrays.equals(a, b, markdownStringEqual); + } + if (isMarkdownString(a) && isMarkdownString(b)) { + return markdownStringEqual(a, b); + } + return false; +} + +function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; + } + return a.value === b.value && a.isTrusted === b.isTrusted; +} + +export function removeMarkdownEscapes(text: string): string { + if (!text) { + return text; + } + return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); +} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts new file mode 100644 index 0000000..39e96b0 --- /dev/null +++ b/src/test/mocks/vsc/index.ts @@ -0,0 +1,596 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { EventEmitter as NodeEventEmitter } from 'events'; +import * as vscode from 'vscode'; + +// export * from './range'; +// export * from './position'; +// export * from './selection'; +export * as vscMockExtHostedTypes from './extHostedTypes'; +export * as vscUri from './uri'; +export * as vscMockCopilotTools from './copilotTools'; + +const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +export function escapeCodicons(text: string): string { + return text.replace(escapeCodiconsRegex, (match, escaped) => (escaped ? match : `\\${match}`)); +} + +export class ThemeIcon { + static readonly File: ThemeIcon; + + static readonly Folder: ThemeIcon; + + constructor(public readonly id: string, public readonly color?: ThemeColor) {} +} + +export class ThemeColor { + constructor(public readonly id: string) {} +} + +export enum ExtensionKind { + /** + * Extension runs where the UI runs. + */ + UI = 1, + + /** + * Extension runs where the remote extension host runs. + */ + Workspace = 2, +} + +export enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2, +} + +export enum QuickPickItemKind { + Separator = -1, + Default = 0, +} + +export class Disposable { + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); + } + } + + disposables = []; + } + }); + } + + private _callOnDispose: (() => void) | undefined; + + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } + + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; + } + } +} + +export namespace l10n { + export function t(message: string, ...args: unknown[]): string; + export function t(options: { + message: string; + args?: Array | Record; + comment: string | string[]; + }): string; + + export function t( + message: + | string + | { + message: string; + args?: Array | Record; + comment: string | string[]; + }, + ...args: unknown[] + ): string { + let _message = message; + let _args: unknown[] | Record | undefined = args; + if (typeof message !== 'string') { + _message = message.message; + _args = message.args ?? args; + } + + if ((_args as Array).length > 0) { + return (_message as string).replace(/{(\d+)}/g, (match, number) => + (_args as Array)[number] === undefined ? match : (_args as Array)[number], + ); + } + return _message as string; + } + export const bundle: { [key: string]: string } | undefined = undefined; + export const uri: vscode.Uri | undefined = undefined; +} + +export class EventEmitter implements vscode.EventEmitter { + public event: vscode.Event; + + public emitter: NodeEventEmitter; + + constructor() { + this.event = this.add.bind(this) as unknown as vscode.Event; + this.emitter = new NodeEventEmitter(); + } + + public fire(data?: T): void { + this.emitter.emit('evt', data); + } + + public dispose(): void { + this.emitter.removeAllListeners(); + } + + protected add = ( + listener: (e: T) => void, + _thisArgs?: EventEmitter, + _disposables?: Disposable[], + ): Disposable => { + const bound = _thisArgs ? listener.bind(_thisArgs) : listener; + this.emitter.addListener('evt', bound); + const disposable = { + dispose: () => { + this.emitter.removeListener('evt', bound); + }, + } as Disposable; + if (_disposables) { + _disposables.push(disposable); + } + return disposable; + }; +} + +export class CancellationToken extends EventEmitter implements vscode.CancellationToken { + public isCancellationRequested!: boolean; + + public onCancellationRequested: vscode.Event; + + constructor() { + super(); + this.onCancellationRequested = this.add.bind(this) as vscode.Event; + } + + public cancel(): void { + this.isCancellationRequested = true; + this.fire(); + } +} + +export class CancellationTokenSource { + public token: CancellationToken; + + constructor() { + this.token = new CancellationToken(); + } + + public cancel(): void { + this.token.cancel(); + } + + public dispose(): void { + this.token.dispose(); + } +} + +export class CodeAction { + public title: string; + + public edit?: vscode.WorkspaceEdit; + + public diagnostics?: vscode.Diagnostic[]; + + public command?: vscode.Command; + + public kind?: CodeActionKind; + + public isPreferred?: boolean; + + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } +} + +export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + Reference = 17, + File = 16, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26, +} +export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} +export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3, +} + +export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2, +} + +export class MarkdownString { + public value: string; + + public isTrusted?: boolean; + + public readonly supportThemeIcons?: boolean; + + constructor(value?: string, supportThemeIcons = false) { + this.value = value ?? ''; + this.supportThemeIcons = supportThemeIcons; + } + + public static isMarkdownString(thing?: string | MarkdownString | unknown): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; + } + return ( + thing !== undefined && + typeof thing === 'object' && + thing !== null && + thing.hasOwnProperty('appendCodeblock') && + thing.hasOwnProperty('appendMarkdown') && + thing.hasOwnProperty('appendText') && + thing.hasOwnProperty('value') + ); + } + + public appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) + .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + .replace(/\n/g, '\n\n'); + + return this; + } + + public appendMarkdown(value: string): MarkdownString { + this.value += value; + + return this; + } + + public appendCodeblock(code: string, language = ''): MarkdownString { + this.value += '\n```'; + this.value += language; + this.value += '\n'; + this.value += code; + this.value += '\n```\n'; + return this; + } +} + +export class Hover { + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + + public range: vscode.Range | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: vscode.Range, + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); + } + if (Array.isArray(contents)) { + this.contents = contents; + } else if (MarkdownString.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; + } + this.range = range; + } +} + +export class CodeActionKind { + public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); + + public static readonly QuickFix: CodeActionKind = new CodeActionKind('quick.fix'); + + public static readonly Refactor: CodeActionKind = new CodeActionKind('refactor'); + + public static readonly RefactorExtract: CodeActionKind = new CodeActionKind('refactor.extract'); + + public static readonly RefactorInline: CodeActionKind = new CodeActionKind('refactor.inline'); + + public static readonly RefactorMove: CodeActionKind = new CodeActionKind('refactor.move'); + + public static readonly RefactorRewrite: CodeActionKind = new CodeActionKind('refactor.rewrite'); + + public static readonly Source: CodeActionKind = new CodeActionKind('source'); + + public static readonly SourceOrganizeImports: CodeActionKind = new CodeActionKind('source.organize.imports'); + + public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); + + public static readonly Notebook: CodeActionKind = new CodeActionKind('notebook'); + + private constructor(private _value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(`${this._value}.${parts}`); + } + + public intersects(other: CodeActionKind): boolean { + return this._value.includes(other._value) || other._value.includes(this._value); + } + + public contains(other: CodeActionKind): boolean { + return this._value.startsWith(other._value); + } + + public get value(): string { + return this._value; + } +} + +export interface DebugAdapterExecutableOptions { + env?: { [key: string]: string }; + cwd?: string; +} + +export class DebugAdapterServer { + constructor(public readonly port: number, public readonly host?: string) {} +} +export class DebugAdapterExecutable { + constructor( + public readonly command: string, + public readonly args: string[] = [], + public readonly options?: DebugAdapterExecutableOptions, + ) {} +} + +export enum FileType { + Unknown = 0, + File = 1, + Directory = 2, + SymbolicLink = 64, +} + +export enum UIKind { + Desktop = 1, + Web = 2, +} + +export class InlayHint { + tooltip?: string | MarkdownString | undefined; + + textEdits?: vscode.TextEdit[]; + + paddingLeft?: boolean; + + paddingRight?: boolean; + + constructor( + public position: vscode.Position, + public label: string | vscode.InlayHintLabelPart[], + 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, +} + +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; + } +} + +/** + * The kind of executions that {@link TestRunProfile TestRunProfiles} control. + */ +export enum TestRunProfileKind { + /** + * The `Run` test profile kind. + */ + Run = 1, + /** + * The `Debug` test profile kind. + */ + Debug = 2, + /** + * The `Coverage` test profile kind. + */ + Coverage = 3, +} diff --git a/src/test/mocks/vsc/position.ts b/src/test/mocks/vsc/position.ts new file mode 100644 index 0000000..0ecb3ea --- /dev/null +++ b/src/test/mocks/vsc/position.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/** + * A position in the editor. This interface is suitable for serialization. + */ +export interface IPosition { + /** + * line number (starts at 1) + */ + readonly lineNumber: number; + /** + * column (the first character in a line is between column 1 and column 2) + */ + readonly column: number; +} + +/** + * A position in the editor. + */ +export class Position { + /** + * line number (starts at 1) + */ + public readonly lineNumber: number; + + /** + * column (the first character in a line is between column 1 and column 2) + */ + public readonly column: number; + + constructor(lineNumber: number, column: number) { + this.lineNumber = lineNumber; + this.column = column; + } + + /** + * Test if this position equals other position + */ + public equals(other: IPosition): boolean { + return Position.equals(this, other); + } + + /** + * Test if position `a` equals position `b` + */ + public static equals(a: IPosition, b: IPosition): boolean { + if (!a && !b) { + return true; + } + return !!a && !!b && a.lineNumber === b.lineNumber && a.column === b.column; + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be false. + */ + public isBefore(other: IPosition): boolean { + return Position.isBefore(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be false. + */ + public static isBefore(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column < b.column; + } + + /** + * Test if this position is before other position. + * If the two positions are equal, the result will be true. + */ + public isBeforeOrEqual(other: IPosition): boolean { + return Position.isBeforeOrEqual(this, other); + } + + /** + * Test if position `a` is before position `b`. + * If the two positions are equal, the result will be true. + */ + public static isBeforeOrEqual(a: IPosition, b: IPosition): boolean { + if (a.lineNumber < b.lineNumber) { + return true; + } + if (b.lineNumber < a.lineNumber) { + return false; + } + return a.column <= b.column; + } + + /** + * A function that compares positions, useful for sorting + */ + public static compare(a: IPosition, b: IPosition): number { + const aLineNumber = a.lineNumber | 0; + const bLineNumber = b.lineNumber | 0; + + if (aLineNumber === bLineNumber) { + const aColumn = a.column | 0; + const bColumn = b.column | 0; + return aColumn - bColumn; + } + + return aLineNumber - bLineNumber; + } + + /** + * Clone this position. + */ + public clone(): Position { + return new Position(this.lineNumber, this.column); + } + + /** + * Convert to a human-readable representation. + */ + public toString(): string { + return `(${this.lineNumber},${this.column})`; + } + + // --- + + /** + * Create a `Position` from an `IPosition`. + */ + public static lift(pos: IPosition): Position { + return new Position(pos.lineNumber, pos.column); + } + + /** + * Test if `obj` is an `IPosition`. + */ + public static isIPosition(obj?: { lineNumber: unknown; column: unknown }): obj is IPosition { + return obj !== undefined && typeof obj.lineNumber === 'number' && typeof obj.column === 'number'; + } +} diff --git a/src/test/mocks/vsc/range.ts b/src/test/mocks/vsc/range.ts new file mode 100644 index 0000000..538e9ec --- /dev/null +++ b/src/test/mocks/vsc/range.ts @@ -0,0 +1,397 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as vscMockPosition from './position'; + +/** + * A range in the editor. This interface is suitable for serialization. + */ +export interface IRange { + /** + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; +} + +/** + * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) + */ +export class Range { + /** + * Line number on which the range starts (starts at 1). + */ + public readonly startLineNumber: number; + + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; + + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; + + /** + * Column on which the range ends in line `endLineNumber`. + */ + public readonly endColumn: number; + + constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + if (startLineNumber > endLineNumber || (startLineNumber === endLineNumber && startColumn > endColumn)) { + this.startLineNumber = endLineNumber; + this.startColumn = endColumn; + this.endLineNumber = startLineNumber; + this.endColumn = startColumn; + } else { + this.startLineNumber = startLineNumber; + this.startColumn = startColumn; + this.endLineNumber = endLineNumber; + this.endColumn = endColumn; + } + } + + /** + * Test if this range is empty. + */ + public isEmpty(): boolean { + return Range.isEmpty(this); + } + + /** + * Test if `range` is empty. + */ + public static isEmpty(range: IRange): boolean { + return range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + } + + /** + * Test if position is in this range. If the position is at the edges, will return true. + */ + public containsPosition(position: vscMockPosition.IPosition): boolean { + return Range.containsPosition(this, position); + } + + /** + * Test if `position` is in `range`. If the position is at the edges, will return true. + */ + public static containsPosition(range: IRange, position: vscMockPosition.IPosition): boolean { + if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { + return false; + } + if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { + return false; + } + if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { + return false; + } + return true; + } + + /** + * Test if range is in this range. If the range is equal to this range, will return true. + */ + public containsRange(range: IRange): boolean { + return Range.containsRange(this, range); + } + + /** + * Test if `otherRange` is in `range`. If the ranges are equal, will return true. + */ + public static containsRange(range: IRange, otherRange: IRange): boolean { + if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) { + return false; + } + if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) { + return false; + } + if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn < range.startColumn) { + return false; + } + if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn > range.endColumn) { + return false; + } + return true; + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IRange): Range { + return Range.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IRange, b: IRange): Range { + let startLineNumber: number; + let startColumn: number; + let endLineNumber: number; + let endColumn: number; + if (b.startLineNumber < a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = b.startColumn; + } else if (b.startLineNumber === a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = Math.min(b.startColumn, a.startColumn); + } else { + startLineNumber = a.startLineNumber; + startColumn = a.startColumn; + } + + if (b.endLineNumber > a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = b.endColumn; + } else if (b.endLineNumber === a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = Math.max(b.endColumn, a.endColumn); + } else { + endLineNumber = a.endLineNumber; + endColumn = a.endColumn; + } + + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } + + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IRange): Range | null { + return Range.intersectRanges(this, range); + } + + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IRange, b: IRange): Range | null { + let resultStartLineNumber = a.startLineNumber; + let resultStartColumn = a.startColumn; + let resultEndLineNumber = a.endLineNumber; + let resultEndColumn = a.endColumn; + const otherStartLineNumber = b.startLineNumber; + const otherStartColumn = b.startColumn; + const otherEndLineNumber = b.endLineNumber; + const otherEndColumn = b.endColumn; + + if (resultStartLineNumber < otherStartLineNumber) { + resultStartLineNumber = otherStartLineNumber; + resultStartColumn = otherStartColumn; + } else if (resultStartLineNumber === otherStartLineNumber) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); + } + + if (resultEndLineNumber > otherEndLineNumber) { + resultEndLineNumber = otherEndLineNumber; + resultEndColumn = otherEndColumn; + } else if (resultEndLineNumber === otherEndLineNumber) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); + } + + // Check if selection is now empty + if (resultStartLineNumber > resultEndLineNumber) { + return null; + } + if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { + return null; + } + + return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + } + + /** + * Test if this range equals other. + */ + public equalsRange(other: IRange): boolean { + return Range.equalsRange(this, other); + } + + /** + * Test if range `a` equals `b`. + */ + public static equalsRange(a: IRange, b: IRange): boolean { + return ( + !!a && + !!b && + a.startLineNumber === b.startLineNumber && + a.startColumn === b.startColumn && + a.endLineNumber === b.endLineNumber && + a.endColumn === b.endColumn + ); + } + + /** + * Return the end position (which will be after or equal to the start position) + */ + public getEndPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.endLineNumber, this.endColumn); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.startLineNumber, this.startColumn); + } + + /** + * Transform to a user presentable string representation. + */ + public toString(): string { + return `[${this.startLineNumber},${this.startColumn} -> ${this.endLineNumber},${this.endColumn}]`; + } + + /** + * Create a new range using this range's start position, and using endLineNumber and endColumn as the end position. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Range { + return new Range(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } + + /** + * Create a new range using this range's end position, and using startLineNumber and startColumn as the start position. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Range { + return new Range(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } + + /** + * Create a new empty range using this range's start position. + */ + public collapseToStart(): Range { + return Range.collapseToStart(this); + } + + /** + * Create a new empty range using this range's start position. + */ + public static collapseToStart(range: IRange): Range { + return new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); + } + + // --- + + public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Range` from an `IRange`. + */ + public static lift(range: IRange): Range | null { + if (!range) { + return null; + } + return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } + + /** + * Test if `obj` is an `IRange`. + */ + public static isIRange(obj?: { + startLineNumber: unknown; + startColumn: unknown; + endLineNumber: unknown; + endColumn: unknown; + }): obj is IRange { + return ( + obj !== undefined && + typeof obj.startLineNumber === 'number' && + typeof obj.startColumn === 'number' && + typeof obj.endLineNumber === 'number' && + typeof obj.endColumn === 'number' + ); + } + + /** + * Test if the two ranges are touching in any way. + */ + public static areIntersectingOrTouching(a: IRange, b: IRange): boolean { + // Check if `a` is before `b` + if ( + a.endLineNumber < b.startLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) + ) { + return false; + } + + // Check if `b` is before `a` + if ( + b.endLineNumber < a.startLineNumber || + (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) + ) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the startPosition and then on the endPosition + */ + public static compareRangesUsingStarts(a: IRange, b: IRange): number { + const aStartLineNumber = a.startLineNumber | 0; + const bStartLineNumber = b.startLineNumber | 0; + + if (aStartLineNumber === bStartLineNumber) { + const aStartColumn = a.startColumn | 0; + const bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + const aEndLineNumber = a.endLineNumber | 0; + const bEndLineNumber = b.endLineNumber | 0; + + if (aEndLineNumber === bEndLineNumber) { + const aEndColumn = a.endColumn | 0; + const bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; + } + return aEndLineNumber - bEndLineNumber; + } + return aStartColumn - bStartColumn; + } + return aStartLineNumber - bStartLineNumber; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the endPosition and then on the startPosition + */ + public static compareRangesUsingEnds(a: IRange, b: IRange): number { + if (a.endLineNumber === b.endLineNumber) { + if (a.endColumn === b.endColumn) { + if (a.startLineNumber === b.startLineNumber) { + return a.startColumn - b.startColumn; + } + return a.startLineNumber - b.startLineNumber; + } + return a.endColumn - b.endColumn; + } + return a.endLineNumber - b.endLineNumber; + } + + /** + * Test if the range spans multiple lines. + */ + public static spansMultipleLines(range: IRange): boolean { + return range.endLineNumber > range.startLineNumber; + } +} diff --git a/src/test/mocks/vsc/strings.ts b/src/test/mocks/vsc/strings.ts new file mode 100644 index 0000000..571b8bc --- /dev/null +++ b/src/test/mocks/vsc/strings.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +/** + * Determines if haystack starts with needle. + */ +export function startsWith(haystack: string, needle: string): boolean { + if (haystack.length < needle.length) { + return false; + } + + for (let i = 0; i < needle.length; i += 1) { + if (haystack[i] !== needle[i]) { + return false; + } + } + + return true; +} + +/** + * Determines if haystack ends with needle. + */ +export function endsWith(haystack: string, needle: string): boolean { + const diff = haystack.length - needle.length; + if (diff > 0) { + return haystack.indexOf(needle, diff) === diff; + } + if (diff === 0) { + return haystack === needle; + } + return false; +} diff --git a/src/test/mocks/vsc/telemetryReporter.ts b/src/test/mocks/vsc/telemetryReporter.ts new file mode 100644 index 0000000..02360e6 --- /dev/null +++ b/src/test/mocks/vsc/telemetryReporter.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export class vscMockTelemetryReporter { + public sendTelemetryEvent(): void { + // Noop. + } +} diff --git a/src/test/mocks/vsc/uri.ts b/src/test/mocks/vsc/uri.ts new file mode 100644 index 0000000..e06b34f --- /dev/null +++ b/src/test/mocks/vsc/uri.ts @@ -0,0 +1,722 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as pathImport from 'path'; +import { CharCode } from './charCode'; + +const isWindows = /^win/.test(process.platform); + +const _schemePattern = /^\w[\w\d+.-]*$/; +const _singleSlashStart = /^\//; +const _doubleSlashStart = /^\/\//; + +const _empty = ''; +const _slash = '/'; +const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; + +const _pathSepMarker = isWindows ? 1 : undefined; + +let _throwOnMissingSchema = true; + +/** + * @internal + */ +export function setUriThrowOnMissingScheme(value: boolean): boolean { + const old = _throwOnMissingSchema; + _throwOnMissingSchema = value; + return old; +} + +function _validateUri(ret: URI, _strict?: boolean): void { + // scheme, must be set + // if (!ret.scheme) { + // // if (_strict || _throwOnMissingSchema) { + // // throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } else { + // console.warn(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } + // } + + // scheme, https://tools.ietf.org/html/rfc3986#section-3.1 + // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + if (ret.scheme && !_schemePattern.test(ret.scheme)) { + throw new Error('[UriError]: Scheme contains illegal characters.'); + } + + // path, http://tools.ietf.org/html/rfc3986#section-3.3 + // If a URI contains an authority component, then the path component + // must either be empty or begin with a slash ("/") character. If a URI + // does not contain an authority component, then the path cannot begin + // with two slash characters ("//"). + if (ret.path) { + if (ret.authority) { + if (!_singleSlashStart.test(ret.path)) { + throw new Error( + '[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character', + ); + } + } else if (_doubleSlashStart.test(ret.path)) { + throw new Error( + '[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")', + ); + } + } +} + +// for a while we allowed uris *without* schemes and this is the migration +// for them, e.g. an uri without scheme and without strict-mode warns and falls +// back to the file-scheme. that should cause the least carnage and still be a +// clear warning +function _schemeFix(scheme: string, _strict: boolean): string { + if (_strict || _throwOnMissingSchema) { + return scheme || _empty; + } + if (!scheme) { + console.trace('BAD uri lacks scheme, falling back to file-scheme.'); + scheme = 'file'; + } + return scheme; +} + +// implements a bit of https://tools.ietf.org/html/rfc3986#section-5 +function _referenceResolution(scheme: string, path: string): string { + // the slash-character is our 'default base' as we don't + // support constructing URIs relative to other URIs. This + // also means that we alter and potentially break paths. + // see https://tools.ietf.org/html/rfc3986#section-5.1.4 + switch (scheme) { + case 'https': + case 'http': + case 'file': + if (!path) { + path = _slash; + } else if (path[0] !== _slash) { + path = _slash + path; + } + break; + default: + break; + } + return path; +} + +/** + * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component parts + * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + */ + +export class URI implements UriComponents { + static isUri(thing: unknown): thing is URI { + if (thing instanceof URI) { + return true; + } + if (!thing) { + return false; + } + return ( + typeof (thing).authority === 'string' && + typeof (thing).fragment === 'string' && + typeof (thing).path === 'string' && + typeof (thing).query === 'string' && + typeof (thing).scheme === 'string' && + typeof (thing).fsPath === 'function' && + typeof (thing).with === 'function' && + typeof (thing).toString === 'function' + ); + } + + /** + * scheme is the 'http' part of 'http://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + readonly scheme: string; + + /** + * authority is the 'www.msft.com' part of 'http://www.msft.com/some/path?query#fragment'. + * The part between the first double slashes and the next slash. + */ + readonly authority: string; + + /** + * path is the '/some/path' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly path: string; + + /** + * query is the 'query' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly query: string; + + /** + * fragment is the 'fragment' part of 'http://www.msft.com/some/path?query#fragment'. + */ + readonly fragment: string; + + /** + * @internal + */ + protected constructor( + scheme: string, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict?: boolean, + ); + + /** + * @internal + */ + protected constructor(components: UriComponents); + + /** + * @internal + */ + protected constructor( + schemeOrData: string | UriComponents, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict = false, + ) { + if (typeof schemeOrData === 'object') { + this.scheme = schemeOrData.scheme || _empty; + this.authority = schemeOrData.authority || _empty; + this.path = schemeOrData.path || _empty; + this.query = schemeOrData.query || _empty; + this.fragment = schemeOrData.fragment || _empty; + // no validation because it's this URI + // that creates uri components. + // _validateUri(this); + } else { + this.scheme = _schemeFix(schemeOrData, _strict); + this.authority = authority || _empty; + this.path = _referenceResolution(this.scheme, path || _empty); + this.query = query || _empty; + this.fragment = fragment || _empty; + + _validateUri(this, _strict); + } + } + + // ---- filesystem path ----------------------- + + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this URI. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `URI#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). + */ + get fsPath(): string { + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } + return _makeFsPath(this); + } + + // ---- modify to new ------------------------- + + with(change: { + scheme?: string; + authority?: string | null; + path?: string | null; + query?: string | null; + fragment?: string | null; + }): URI { + if (!change) { + return this; + } + + let { scheme, authority, path, query, fragment } = change; + if (scheme === undefined) { + scheme = this.scheme; + } else if (scheme === null) { + scheme = _empty; + } + if (authority === undefined) { + authority = this.authority; + } else if (authority === null) { + authority = _empty; + } + if (path === undefined) { + path = this.path; + } else if (path === null) { + path = _empty; + } + if (query === undefined) { + query = this.query; + } else if (query === null) { + query = _empty; + } + if (fragment === undefined) { + fragment = this.fragment; + } else if (fragment === null) { + fragment = _empty; + } + + if ( + scheme === this.scheme && + authority === this.authority && + path === this.path && + query === this.query && + fragment === this.fragment + ) { + return this; + } + + return new _URI(scheme, authority, path, query, fragment); + } + + // ---- parse & validate ------------------------ + + /** + * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + * @param {boolean} [_strict=false] + */ + static parse(value: string, _strict = false): URI { + const match = _regexp.exec(value); + if (!match) { + return new _URI(_empty, _empty, _empty, _empty, _empty); + } + return new _URI( + match[2] || _empty, + decodeURIComponent(match[4] || _empty), + decodeURIComponent(match[5] || _empty), + decodeURIComponent(match[7] || _empty), + decodeURIComponent(match[9] || _empty), + _strict, + ); + } + + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** + * `URI.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = URI.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + const bad = URI.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` + * + * @param path A file system path (see `URI#fsPath`) + */ + static file(path: string): URI { + let authority = _empty; + + // normalize to fwd-slashes on windows, + // on other systems bwd-slashes are valid + // filename character, eg /f\oo/ba\r.txt + if (isWindows) { + path = path.replace(/\\/g, _slash); + } + + // check for authority as used in UNC shares + // or use the path as given + if (path[0] === _slash && path[1] === _slash) { + const idx = path.indexOf(_slash, 2); + if (idx === -1) { + authority = path.substring(2); + path = _slash; + } else { + authority = path.substring(2, idx); + path = path.substring(idx) || _slash; + } + } + + return new _URI('file', authority, path, _empty, _empty); + } + + static from(components: { + scheme: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): URI { + return new _URI( + components.scheme, + components.authority, + components.path, + components.query, + components.fragment, + ); + } + + // ---- printing/externalize --------------------------- + + /** + * Creates a string representation for this URI. It's guaranteed that calling + * `URI.parse` with the result of this function creates an URI which is equal + * to this URI. + * + * * The result shall *not* be used for display purposes but for externalization or transport. + * * The result will be encoded using the percentage encoding and encoding happens mostly + * ignore the scheme-specific encoding rules. + * + * @param skipEncoding Do not encode the result, default is `false` + */ + toString(skipEncoding = false): string { + return _asFormatted(this, skipEncoding); + } + + toJSON(): UriComponents { + return this; + } + + static revive(data: UriComponents | URI): URI; + + static revive(data: UriComponents | URI | undefined): URI | undefined; + + static revive(data: UriComponents | URI | null): URI | null; + + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; + + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null { + if (!data) { + return data; + } + if (data instanceof URI) { + return data; + } + const result = new _URI(data); + result._formatted = (data).external; + result._fsPath = (data)._sep === _pathSepMarker ? (data).fsPath : null; + return result; + } + + static joinPath(uri: URI, ...pathFragment: string[]): URI { + if (!uri.path) { + throw new Error(`[UriError]: cannot call joinPaths on URI without path`); + } + let newPath: string; + if (isWindows && uri.scheme === 'file') { + newPath = URI.file(pathImport.join(uri.fsPath, ...pathFragment)).path; + } else { + newPath = pathImport.join(uri.path, ...pathFragment); + } + return uri.with({ path: newPath }); + } +} + +export interface UriComponents { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; +} + +interface UriState extends UriComponents { + $mid: number; + external: string; + fsPath: string; + _sep: 1 | undefined; +} + +class _URI extends URI { + _formatted: string | null = null; + + _fsPath: string | null = null; + + constructor( + schemeOrData: string | UriComponents, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict = false, + ) { + super(schemeOrData as string, authority, path, query, fragment, _strict); + this._fsPath = this.fsPath; + } + + get fsPath(): string { + if (!this._fsPath) { + this._fsPath = _makeFsPath(this); + } + return this._fsPath; + } + + toString(skipEncoding = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); + } + return this._formatted; + } + // we don't cache that + return _asFormatted(this, true); + } + + toJSON(): UriComponents { + const res = { + $mid: 1, + }; + // cached state + if (this._fsPath) { + res.fsPath = this._fsPath; + if (_pathSepMarker) { + res._sep = _pathSepMarker; + } + } + if (this._formatted) { + res.external = this._formatted; + } + // uri components + if (this.path) { + res.path = this.path; + } + if (this.scheme) { + res.scheme = this.scheme; + } + if (this.authority) { + res.authority = this.authority; + } + if (this.query) { + res.query = this.query; + } + if (this.fragment) { + res.fragment = this.fragment; + } + return res; + } +} + +// reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2 +const encodeTable: { [ch: number]: string } = { + [CharCode.Colon]: '%3A', // gen-delims + [CharCode.Slash]: '%2F', + [CharCode.QuestionMark]: '%3F', + [CharCode.Hash]: '%23', + [CharCode.OpenSquareBracket]: '%5B', + [CharCode.CloseSquareBracket]: '%5D', + [CharCode.AtSign]: '%40', + + [CharCode.ExclamationMark]: '%21', // sub-delims + [CharCode.DollarSign]: '%24', + [CharCode.Ampersand]: '%26', + [CharCode.SingleQuote]: '%27', + [CharCode.OpenParen]: '%28', + [CharCode.CloseParen]: '%29', + [CharCode.Asterisk]: '%2A', + [CharCode.Plus]: '%2B', + [CharCode.Comma]: '%2C', + [CharCode.Semicolon]: '%3B', + [CharCode.Equals]: '%3D', + + [CharCode.Space]: '%20', +}; + +function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string { + let res: string | undefined; + let nativeEncodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos += 1) { + const code = uriComponent.charCodeAt(pos); + + // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3 + if ( + (code >= CharCode.a && code <= CharCode.z) || + (code >= CharCode.A && code <= CharCode.Z) || + (code >= CharCode.Digit0 && code <= CharCode.Digit9) || + code === CharCode.Dash || + code === CharCode.Period || + code === CharCode.Underline || + code === CharCode.Tilde || + (allowSlash && code === CharCode.Slash) + ) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); + } + } else { + // encoding needed, we need to allocate a new string + if (res === undefined) { + res = uriComponent.substr(0, pos); + } + + // check with default table first + const escaped = encodeTable[code]; + if (escaped !== undefined) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + + // append escaped variant to result + res += escaped; + } else if (nativeEncodePos === -1) { + // use native encode only when needed + nativeEncodePos = pos; + } + } + } + + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); + } + + return res !== undefined ? res : uriComponent; +} + +function encodeURIComponentMinimal(path: string): string { + let res: string | undefined; + for (let pos = 0; pos < path.length; pos += 1) { + const code = path.charCodeAt(pos); + if (code === CharCode.Hash || code === CharCode.QuestionMark) { + if (res === undefined) { + res = path.substr(0, pos); + } + res += encodeTable[code]; + } else if (res !== undefined) { + res += path[pos]; + } + } + return res !== undefined ? res : path; +} + +/** + * Compute `fsPath` for the given uri + */ +function _makeFsPath(uri: URI): string { + let value: string; + if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { + // unc path: file://shares/c$/far/boo + value = `//${uri.authority}${uri.path}`; + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash && + ((uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z) || + (uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z)) && + uri.path.charCodeAt(2) === CharCode.Colon + ) { + // windows drive letter: file:///c:/far/boo + value = uri.path[1].toLowerCase() + uri.path.substr(2); + } else { + // other path + value = uri.path; + } + if (isWindows) { + value = value.replace(/\//g, '\\'); + } + return value; +} + +/** + * Create the external version of a uri + */ +function _asFormatted(uri: URI, skipEncoding: boolean): string { + const encoder = !skipEncoding ? encodeURIComponentFast : encodeURIComponentMinimal; + + let res = ''; + let { authority, path } = uri; + const { scheme, query, fragment } = uri; + if (scheme) { + res += scheme; + res += ':'; + } + if (authority || scheme === 'file') { + res += _slash; + res += _slash; + } + if (authority) { + let idx = authority.indexOf('@'); + if (idx !== -1) { + // @ + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); + if (idx === -1) { + res += encoder(userinfo, false); + } else { + // :@ + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); + } + res += '@'; + } + authority = authority.toLowerCase(); + idx = authority.indexOf(':'); + if (idx === -1) { + res += encoder(authority, false); + } else { + // : + res += encoder(authority.substr(0, idx), false); + res += authority.substr(idx); + } + } + if (path) { + // lower-case windows drive letters in /C:/fff or C:/fff + if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) { + const code = path.charCodeAt(1); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3 + } + } else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) { + const code = path.charCodeAt(0); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3 + } + } + // encode the rest of the path + res += encoder(path, true); + } + if (query) { + res += '?'; + res += encoder(query, false); + } + if (fragment) { + res += '#'; + res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; + } + return res; +} diff --git a/src/test/mocks/vsc/uuid.ts b/src/test/mocks/vsc/uuid.ts new file mode 100644 index 0000000..05176df --- /dev/null +++ b/src/test/mocks/vsc/uuid.ts @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Represents a UUID as defined by rfc4122. + */ + +export interface UUID { + /** + * @returns the canonical representation in sets of hexadecimal numbers separated by dashes. + */ + asHex(): string; +} + +class ValueUUID implements UUID { + constructor(public _value: string) { + // empty + } + + public asHex(): string { + return this._value; + } +} + +class V4UUID extends ValueUUID { + private static readonly _chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; + + private static readonly _timeHighBits = ['8', '9', 'a', 'b']; + + private static _oneOf(array: string[]): string { + return array[Math.floor(array.length * Math.random())]; + } + + private static _randomHex(): string { + return V4UUID._oneOf(V4UUID._chars); + } + + constructor() { + super( + [ + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + '4', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._oneOf(V4UUID._timeHighBits), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + '-', + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + V4UUID._randomHex(), + ].join(''), + ); + } +} + +export function v4(): UUID { + return new V4UUID(); +} + +const UUIDPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function isUUID(value: string): boolean { + return UUIDPattern.test(value); +} + +/** + * Parses a UUID that is of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. + * @param value A uuid string. + */ +export function parse(value: string): UUID { + if (!isUUID(value)) { + throw new Error('invalid uuid'); + } + + return new ValueUUID(value); +} + +export function generateUuid(): string { + return v4().asHex(); +} diff --git a/src/test/unittests.ts b/src/test/unittests.ts new file mode 100644 index 0000000..94a5546 --- /dev/null +++ b/src/test/unittests.ts @@ -0,0 +1,143 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { anything, instance, mock, when } from 'ts-mockito'; +import * as vscode from 'vscode'; +import * as vscodeMocks from './mocks/vsc'; +import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +const Module = require('module'); + +type VSCode = typeof vscode; + +const mockedVSCode: Partial = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; +const originalLoad = Module._load; + +function generateMock(name: K): void { + const mockedObj = mock(); + (mockedVSCode as any)[name] = instance(mockedObj); + mockedVSCodeNamespaces[name] = mockedObj as any; +} + +class MockClipboard { + private text: string = ''; + public readText(): Promise { + return Promise.resolve(this.text); + } + public async writeText(value: string): Promise { + this.text = value; + } +} +export function initialize() { + generateMock('workspace'); + generateMock('window'); + generateMock('commands'); + generateMock('languages'); + generateMock('extensions'); + generateMock('env'); + generateMock('debug'); + generateMock('scm'); + generateMock('notebooks'); + generateMock('tasks'); + + // Use mock clipboard fo testing purposes. + const clipboard = new MockClipboard(); + 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) { + if (request === 'vscode') { + return mockedVSCode; + } + if (request === '@vscode/extension-telemetry') { + return { default: vscMockTelemetryReporter as any }; + } + // less files need to be in import statements to be converted to css + // But we don't want to try to load them in the mock vscode + if (/\.less$/.test(request)) { + return; + } + return originalLoad.apply(this, arguments); + }; +} + +mockedVSCode.ThemeIcon = vscodeMocks.ThemeIcon; +mockedVSCode.l10n = vscodeMocks.l10n; +mockedVSCode.ThemeColor = vscodeMocks.ThemeColor; +mockedVSCode.MarkdownString = vscodeMocks.MarkdownString; +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; +mockedVSCode.SymbolKind = vscodeMocks.SymbolKind; +mockedVSCode.IndentAction = vscodeMocks.IndentAction; +mockedVSCode.Uri = vscodeMocks.vscUri.URI as any; +mockedVSCode.Range = vscodeMocks.vscMockExtHostedTypes.Range; +mockedVSCode.Position = vscodeMocks.vscMockExtHostedTypes.Position; +mockedVSCode.Selection = vscodeMocks.vscMockExtHostedTypes.Selection; +mockedVSCode.Location = vscodeMocks.vscMockExtHostedTypes.Location; +mockedVSCode.SymbolInformation = vscodeMocks.vscMockExtHostedTypes.SymbolInformation; +mockedVSCode.CallHierarchyItem = vscodeMocks.vscMockExtHostedTypes.CallHierarchyItem; +mockedVSCode.CompletionItem = vscodeMocks.vscMockExtHostedTypes.CompletionItem; +mockedVSCode.CompletionItemKind = vscodeMocks.vscMockExtHostedTypes.CompletionItemKind; +mockedVSCode.CodeLens = vscodeMocks.vscMockExtHostedTypes.CodeLens; +mockedVSCode.Diagnostic = vscodeMocks.vscMockExtHostedTypes.Diagnostic; +mockedVSCode.DiagnosticSeverity = vscodeMocks.vscMockExtHostedTypes.DiagnosticSeverity; +mockedVSCode.SnippetString = vscodeMocks.vscMockExtHostedTypes.SnippetString; +mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; +mockedVSCode.StatusBarAlignment = vscodeMocks.vscMockExtHostedTypes.StatusBarAlignment; +mockedVSCode.SignatureHelp = vscodeMocks.vscMockExtHostedTypes.SignatureHelp; +mockedVSCode.DocumentLink = vscodeMocks.vscMockExtHostedTypes.DocumentLink; +mockedVSCode.TextEdit = vscodeMocks.vscMockExtHostedTypes.TextEdit; +mockedVSCode.WorkspaceEdit = vscodeMocks.vscMockExtHostedTypes.WorkspaceEdit; +mockedVSCode.RelativePattern = vscodeMocks.vscMockExtHostedTypes.RelativePattern; +mockedVSCode.ProgressLocation = vscodeMocks.vscMockExtHostedTypes.ProgressLocation; +mockedVSCode.ViewColumn = vscodeMocks.vscMockExtHostedTypes.ViewColumn; +mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditorRevealType; +mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; +mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; +(mockedVSCode as any).CodeActionKind = vscodeMocks.CodeActionKind; +mockedVSCode.CompletionItemKind = vscodeMocks.CompletionItemKind; +mockedVSCode.CompletionTriggerKind = vscodeMocks.CompletionTriggerKind; +mockedVSCode.DebugAdapterExecutable = vscodeMocks.DebugAdapterExecutable; +mockedVSCode.DebugAdapterServer = vscodeMocks.DebugAdapterServer; +mockedVSCode.QuickInputButtons = vscodeMocks.vscMockExtHostedTypes.QuickInputButtons; +mockedVSCode.FileType = vscodeMocks.FileType; +mockedVSCode.UIKind = vscodeMocks.UIKind; +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; +(mockedVSCode as any).TypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.TypeHierarchyItem; +(mockedVSCode as any).ProtocolTypeHierarchyItem = vscodeMocks.vscMockExtHostedTypes.ProtocolTypeHierarchyItem; +(mockedVSCode as any).CancellationError = vscodeMocks.vscMockExtHostedTypes.CancellationError; +(mockedVSCode as any).LSPCancellationError = vscodeMocks.vscMockExtHostedTypes.LSPCancellationError; +mockedVSCode.TestRunProfileKind = vscodeMocks.TestRunProfileKind; +mockedVSCode.LanguageModelTextPart = vscodeMocks.vscMockCopilotTools.LanguageModelTextPart; + +// Task-related mocks +mockedVSCode.Task = vscodeMocks.vscMockExtHostedTypes.Task; +mockedVSCode.TaskScope = vscodeMocks.vscMockExtHostedTypes.TaskScope; +mockedVSCode.ShellExecution = vscodeMocks.vscMockExtHostedTypes.ShellExecution; +mockedVSCode.TaskRevealKind = vscodeMocks.vscMockExtHostedTypes.TaskRevealKind; +mockedVSCode.TaskPanelKind = vscodeMocks.vscMockExtHostedTypes.TaskPanelKind; + +initialize(); diff --git a/src/vscode.proposed.terminalShellEnv.d.ts b/src/vscode.proposed.terminalShellEnv.d.ts new file mode 100644 index 0000000..13f4635 --- /dev/null +++ b/src/vscode.proposed.terminalShellEnv.d.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + // @anthonykim1 @tyriar https://github.com/microsoft/vscode/issues/227467 + + export interface TerminalShellIntegrationEnvironment { + /** + * The dictionary of environment variables. + */ + value: { [key: string]: string | undefined } | undefined; + + /** + * Whether the environment came from a trusted source and is therefore safe to use its + * values in a manner that could lead to execution of arbitrary code. If this value is + * `false`, {@link value} should either not be used for something that could lead to arbitrary + * code execution, or the user should be warned beforehand. + * + * This is `true` only when the environment was reported explicitly and it used a nonce for + * verification. + */ + isTrusted: boolean; + } + + export interface TerminalShellIntegration { + /** + * The environment of the shell process. This is undefined if the shell integration script + * does not send the environment. + */ + readonly env: TerminalShellIntegrationEnvironment; + } + + // TODO: Is it fine that this shares onDidChangeTerminalShellIntegration with cwd and the shellIntegration object itself? +} diff --git a/tsconfig.json b/tsconfig.json index 1a96fa0..f33234f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,9 @@ { - "compilerOptions": { + "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", - "target": "ES2020", - "lib": [ - "ES2020" - ], + "target": "ES2020", + "lib": ["ES2020"], "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, @@ -18,5 +16,6 @@ "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, "removeComments": true - } + }, + "exclude": ["examples"] }