Skip to content

Commit e74470f

Browse files
authored
updates to project manager docs and comments (#387)
added a doc explaining how the project technical design works add comments and docstrings for clarity
1 parent 2048abe commit e74470f

File tree

2 files changed

+106
-17
lines changed

2 files changed

+106
-17
lines changed

docs/projects-api-reference.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Python Projects API Reference
2+
3+
## Modifying Projects
4+
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.
5+
6+
1. **API Call:** Extensions can calls `getPythonProjects` on [`PythonEnvironmentApi`](../src/api.ts).
7+
2. **API Implementation:** [`PythonEnvironmentApiImpl`](../src/features/pythonApi.ts) delegates to its internal project manager.
8+
3. **Internal API:** The project manager is typed as [`PythonProjectManager`](../src/internal.api.ts).
9+
4. **Concrete Implementation:** [`PythonProjectManagerImpl`](../src/features/projectManager.ts) implements the actual logic.
10+
5. **Data Model:** Returns an array of [`PythonProject`](../src/api.ts) objects.
11+
12+
## Project Creators
13+
This is how creating projects work with the API as it uses a method of registering
14+
external or internal project creators and maintaining project states internally in
15+
just this extension.
16+
17+
- **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.
18+
- **Registration:** Project creators are registered with the API, making them discoverable and usable by the extension or other consumers.
19+
- **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.
20+
21+
### What an Extension Must Do
22+
23+
1. **Implement the Creator Interface:**
24+
- Create a class that implements the [`PythonProjectCreator`](../src/api.ts) interface.
25+
- Provide a unique `name`, a user-friendly `displayName`, and a `create` method that returns one or more `PythonProject` objects or URIs.
26+
27+
2. **Register the Creator:**
28+
- Register the creator with the main API (usually via a registration method exposed by this extension’s API surface).
29+
- This makes the creator discoverable and usable by the extension and other consumers.
30+
31+
3. **Add Projects Directly:**
32+
- 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.
33+
34+
35+
### Responsibilities Table
36+
37+
| Step | External Extension’s Responsibility | Internal Python-Envs-Ext Responsibility |
38+
| ------------------------------------------ | :---------------------------------: | :-------------------------------------: |
39+
| Implement `PythonProjectCreator` interface | ☑️ | |
40+
| Register the creator | ☑️ | |
41+
| Provide `create` method | ☑️ | |
42+
| Add projects to project manager | ☑️ | |
43+
| Update project settings | | ☑️ |
44+
| Track and list creators | | ☑️ |
45+
| Invoke creator and handle results | | ☑️ |
46+
47+
48+
### Example Implementation: [`ExistingProjects`](../src/features/creators/existingProjects.ts)
49+
50+
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:
51+
52+
create function implementation abbreviated:
53+
```typescript
54+
async create(
55+
_options?: PythonProjectCreatorOptions,
56+
): Promise<PythonProject | PythonProject[] | Uri | Uri[] | undefined> {
57+
const projects = resultsInWorkspace.map(
58+
(uri) => new PythonProjectsImpl(path.basename(uri.fsPath), uri),
59+
) as PythonProject[];
60+
this.pm.add(projects);
61+
return projects;
62+
}
63+
```
64+
65+
creator registration (usually on extension activation):
66+
```
67+
projectCreators.registerPythonProjectCreator(new ExistingProjects(projectManager)),
68+
69+
```
70+
71+
- **Implements:** [`PythonProjectCreator`](../src/api.ts)
72+
- **Adds projects to:** `PythonProjectManager` (see below)
73+
74+
75+

src/features/projectManager.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
2424
private _projects = new Map<string, PythonProject>();
2525
private readonly _onDidChangeProjects = new EventEmitter<ProjectArray | undefined>();
2626
public readonly onDidChangeProjects = this._onDidChangeProjects.event;
27+
28+
// Debounce the updateProjects method to avoid excessive update calls
2729
private readonly updateDebounce = createSimpleDebounce(100, () => this.updateProjects());
2830

2931
initialize(): void {
@@ -46,17 +48,24 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
4648
);
4749
}
4850

51+
/**
52+
*
53+
* Gathers the projects which are configured in settings and all workspace roots.
54+
* @returns An array of PythonProject objects representing the initial projects.
55+
*/
4956
private getInitialProjects(): ProjectArray {
5057
const newProjects: ProjectArray = [];
5158
const workspaces = getWorkspaceFolders() ?? [];
5259
for (const w of workspaces) {
5360
const config = getConfiguration('python-envs', w.uri);
5461
const overrides = config.get<PythonProjectSettings[]>('pythonProjects', []);
5562

63+
// Add the workspace root as a project if not already present
5664
if (!newProjects.some((p) => p.uri.toString() === w.uri.toString())) {
5765
newProjects.push(new PythonProjectsImpl(w.name, w.uri));
5866
}
5967

68+
// For each override, resolve its path and add as a project if not already present
6069
for (const o of overrides) {
6170
const uri = Uri.file(path.resolve(w.uri.fsPath, o.path));
6271
if (!newProjects.some((p) => p.uri.toString() === uri.toString())) {
@@ -67,31 +76,22 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
6776
return newProjects;
6877
}
6978

79+
/**
80+
* Get initial projects from the workspace(s) config settings
81+
* then updates the internal _projects map to reflect the current state and
82+
* fires the onDidChangeProjects event if there are any changes.
83+
*/
7084
private updateProjects(): void {
71-
const workspaces = getWorkspaceFolders() ?? [];
85+
const newProjects: ProjectArray = this.getInitialProjects();
7286
const existingProjects = Array.from(this._projects.values());
73-
const newProjects: ProjectArray = [];
74-
75-
for (const w of workspaces) {
76-
const config = getConfiguration('python-envs', w.uri);
77-
const overrides = config.get<PythonProjectSettings[]>('pythonProjects', []);
78-
79-
if (!newProjects.some((p) => p.uri.toString() === w.uri.toString())) {
80-
newProjects.push(new PythonProjectsImpl(w.name, w.uri));
81-
}
82-
for (const o of overrides) {
83-
const uri = Uri.file(path.resolve(w.uri.fsPath, o.path));
84-
if (!newProjects.some((p) => p.uri.toString() === uri.toString())) {
85-
newProjects.push(new PythonProjectsImpl(o.path, uri));
86-
}
87-
}
88-
}
8987

88+
// Remove projects that are no longer in the workspace settings
9089
const projectsToRemove = existingProjects.filter(
9190
(w) => !newProjects.find((n) => n.uri.toString() === w.uri.toString()),
9291
);
9392
projectsToRemove.forEach((w) => this._projects.delete(w.uri.toString()));
9493

94+
// Add new projects that are in the workspace settings but not in the existing projects
9595
const projectsToAdd = newProjects.filter(
9696
(n) => !existingProjects.find((w) => w.uri.toString() === n.uri.toString()),
9797
);
@@ -136,6 +136,7 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
136136
// for non-root projects, always add setting
137137
edits.push({ project: currProject, envManager: envManagerId, packageManager: pkgManagerId });
138138
}
139+
// handles adding the project to this._projects map
139140
return this._projects.set(currProject.uri.toString(), currProject);
140141
});
141142
this._onDidChangeProjects.fire(Array.from(this._projects.values()));
@@ -156,6 +157,7 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
156157
}
157158

158159
getProjects(uris?: Uri[]): ReadonlyArray<PythonProject> {
160+
console.log('getProjects', uris);
159161
if (uris === undefined) {
160162
return Array.from(this._projects.values());
161163
} else {
@@ -178,6 +180,11 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
178180
return pythonProject;
179181
}
180182

183+
/**
184+
* Finds the single project that matches the given URI if it exists.
185+
* @param uri The URI of the project to find.
186+
* @returns The project with the given URI, or undefined if not found.
187+
*/
181188
private findProjectByUri(uri: Uri): PythonProject | undefined {
182189
const _projects = Array.from(this._projects.values()).sort((a, b) => b.uri.fsPath.length - a.uri.fsPath.length);
183190

@@ -191,6 +198,13 @@ export class PythonProjectManagerImpl implements PythonProjectManager {
191198
return undefined;
192199
}
193200

201+
/**
202+
* Checks if a given file or folder path (normalizedUriPath)
203+
* is the same as, or is inside, a project path
204+
* @normalizedProjectPath Project path to check against.
205+
* @normalizedUriPath File or folder path to check.
206+
* @returns true if the file or folder path is the same as or inside the project path, false otherwise.
207+
*/
194208
private isUriMatching(normalizedUriPath: string, normalizedProjectPath: string): boolean {
195209
if (normalizedProjectPath === normalizedUriPath) {
196210
return true;

0 commit comments

Comments
 (0)