Skip to content

Commit 2182dc7

Browse files
committed
fix: fixes for pip resource + tests
1 parent 2e0de38 commit 2182dc7

File tree

2 files changed

+108
-62
lines changed

2 files changed

+108
-62
lines changed

src/resources/python/pip/pip.ts

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import {
22
CreatePlan,
33
DestroyPlan,
4+
getPty,
45
ModifyPlan,
56
ParameterChange,
6-
RefreshContext,
77
Resource,
8-
ResourceSettings,
9-
getPty
8+
ResourceSettings
109
} from 'codify-plugin-lib';
1110
import { ResourceConfig } from 'codify-schemas';
1211

@@ -32,24 +31,20 @@ export class PipResource extends Resource<PipResourceConfig> {
3231
type: 'array',
3332
itemType: 'object',
3433
canModify: true,
35-
isElementEqual(desired: PipListResult | string, current: PipListResult | string) {
36-
if (typeof desired === 'string' && typeof current === 'string') {
37-
return desired === current;
38-
}
39-
40-
// We can do this check because of the pre-filtering we are doing in refresh. It converts the current to match the desired if it is defined.
41-
return (desired as PipListResult).name === (current as PipListResult).name;
42-
}
34+
isElementEqual: this.isEqual,
35+
filterInStatelessMode: (desired, current) =>
36+
current.filter((c) => desired.find((d) => this.isSame(c, d)))
4337
},
4438
virtualEnv: { type: 'directory' }
4539
},
4640
allowMultiple: {
4741
identifyingParameters: ['virtualEnv']
48-
}
42+
},
43+
dependencies: ['pyenv', 'git-repository']
4944
}
5045
}
5146

52-
async refresh(parameters: Partial<PipResourceConfig>, context: RefreshContext<PipResourceConfig>): Promise<Partial<PipResourceConfig> | Partial<PipResourceConfig>[] | null> {
47+
async refresh(parameters: Partial<PipResourceConfig>): Promise<Partial<PipResourceConfig> | Partial<PipResourceConfig>[] | null> {
5348
const pty = getPty()
5449

5550
const { status: pipStatus } = await pty.spawnSafe('which pip');
@@ -93,14 +88,13 @@ export class PipResource extends Resource<PipResourceConfig> {
9388
return { name, version };
9489
});
9590

96-
console.log(parsedInstalledPackages);
97-
9891
return {
9992
...parameters,
10093
install: parsedInstalledPackages,
10194
}
10295
}
10396

97+
// Pip cannot be individually installed. It's installed via installing python. This only installs packages when python is first created.
10498
async create(plan: CreatePlan<PipResourceConfig>): Promise<void> {
10599
const { install, virtualEnv } = plan.desiredConfig;
106100

@@ -123,11 +117,8 @@ export class PipResource extends Resource<PipResourceConfig> {
123117
}
124118
}
125119

126-
async destroy(plan: DestroyPlan<PipResourceConfig>): Promise<void> {
127-
const { install, virtualEnv } = plan.currentConfig;
128-
129-
await this.pipUninstall(install, virtualEnv);
130-
}
120+
// Pip cannot be individually destroyed.
121+
async destroy(plan: DestroyPlan<PipResourceConfig>): Promise<void> {}
131122

132123
private async pipInstall(packages: Array<PipListResult | string>, virtualEnv?: string): Promise<void> {
133124
const packagesToInstall = packages.map((p) => {
@@ -159,29 +150,43 @@ export class PipResource extends Resource<PipResourceConfig> {
159150

160151
await codifySpawn(
161152
(virtualEnv ? `source ${virtualEnv}/bin/activate; ` : '')
162-
+ `pip install ${packagesToUninstall.join(' ')}`
153+
+ `pip uninstall -y ${packagesToUninstall.join(' ')}`
163154
)
164155
}
165156

166-
findMatchingForModify(d: PipListResult | string, cList: Array<PipListResult | string>): PipListResult | string | undefined {
167-
return cList.find((c) => {
168-
if (typeof d === 'string' && typeof c === 'string') {
169-
return d === c;
170-
}
157+
findMatchingForModify(a: PipListResult | string, bList: Array<PipListResult | string>): PipListResult | string | undefined {
158+
return bList.find((b) => this.isEqual(a, b))
159+
}
171160

172-
if (!(typeof d === 'object' && typeof c === 'object')) {
173-
return false;
174-
}
161+
isEqual(a: PipListResult | string, b: PipListResult | string): boolean {
162+
if (typeof a === 'string' && typeof b === 'string') {
163+
return a === b;
164+
}
175165

176-
if (d.name !== c.name) {
177-
return false;
178-
}
166+
if (!(typeof a === 'object' && typeof b === 'object')) {
167+
return false;
168+
}
179169

180-
if (d.version && d.version !== c.version) {
181-
return false
182-
}
170+
if (a.name !== b.name) {
171+
return false;
172+
}
173+
174+
if (a.version && a.version !== b.version) {
175+
return false
176+
}
177+
178+
return true;
179+
}
180+
181+
isSame(a: PipListResult | string, b: PipListResult | string): boolean {
182+
if (typeof a === 'string' && typeof b === 'string') {
183+
return a === b;
184+
}
185+
186+
if (!(typeof a === 'object' && typeof b === 'object')) {
187+
return false;
188+
}
183189

184-
return true;
185-
})
190+
return a.name === b.name;
186191
}
187192
}

test/python/pip.test.ts

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,89 @@ import * as path from 'node:path';
44
import { execSync } from 'child_process';
55
import fs from 'node:fs';
66
import os from 'node:os';
7+
import { ResourceOperation } from 'codify-schemas';
78

8-
describe('Pyenv resource integration tests', () => {
9+
describe('Pip resource integration tests', () => {
910
const pluginPath = path.resolve('./src/index.ts');
1011

11-
it('Installs pyenv and python (this installs on a clean system without readline, openSSL, etc.)', { timeout: 500000 }, async () => {
12+
it('Installs python', { timeout: 500000 }, async () => {
1213
await PluginTester.fullTest(pluginPath, [
1314
{
1415
type: 'pyenv',
15-
pythonVersions: ['3.11']
16-
}
16+
pythonVersions: ['3.11'],
17+
global: '3.11'
18+
},
1719
], {
1820
skipUninstall: true,
1921
validateApply: () => {
20-
expect(() => execSync('source ~/.zshrc; which pyenv', { shell: 'zsh' })).to.not.throw();
22+
expect(execSync('source ~/.zshrc; python --version', { shell: 'zsh' }).toString()).to.include('3.11');
2123
}
22-
});
23-
});
24+
})
25+
})
2426

25-
it ('Can install additional python versions. (this installs after openSSL and readline have been installed)', { timeout: 700000 }, async () => {
27+
it('Installs python and installs a package using pip', { timeout: 300000 }, async () => {
2628
await PluginTester.fullTest(pluginPath, [
2729
{
28-
type: 'homebrew',
29-
formulae: ['readline', 'openssl@3']
30-
},
31-
{
32-
type: 'pyenv',
33-
pythonVersions: ['3.11', '3.12'],
34-
global: '3.12',
30+
type: 'pip',
31+
install: [
32+
'ffmpeg',
33+
{ name: 'qoverage', version: "0.1.12"},
34+
]
3535
}
3636
], {
37+
skipUninstall: true,
38+
validatePlan: (plans) => {
39+
console.log(JSON.stringify(plans, null, 2))
40+
},
3741
validateApply: () => {
38-
expect(execSync('source ~/.zshrc; python --version', { shell: 'zsh' }).toString('utf-8')).to.include('3.12');
42+
const installedDependencies = execSync('source ~/.zshrc; pip list --format=json --disable-pip-version-check', { shell: 'zsh' }).toString()
43+
const parsed = JSON.parse(installedDependencies) as Array<{ name: string; version: string; }>;
3944

40-
const versions = execSync('source ~/.zshrc; pyenv versions', { shell: 'zsh' }).toString('utf-8')
41-
expect(versions).to.include('3.12')
42-
expect(versions).to.include('3.11')
45+
expect(parsed.some((p) => p.name === 'ffmpeg')).to.be.true;
46+
expect(parsed.some((p) => p.name === 'qoverage' && p.version === '0.1.12')).to.be.true;
4347
},
44-
validateDestroy: () => {
45-
expect(fs.existsSync(path.resolve(os.homedir(), '.pyenv'))).to.be.false;
46-
expect(fs.readFileSync(path.resolve(os.homedir(), '.zshrc'), 'utf-8')).to.not.contain('pyenv');
47-
expect(() => execSync('source ~/.zshrc; which pyenv', { shell: 'zsh' })).to.throw();
48+
testModify: {
49+
modifiedConfigs: [
50+
{
51+
type: 'pip',
52+
install: [
53+
'ffmpeg',
54+
'ansible-roster',
55+
{ name: 'qoverage', version: "0.1.13"},
56+
]
57+
}
58+
],
59+
validateModify: (plans) => {
60+
expect(plans.length).to.eq(1);
61+
const plan = plans[0];
62+
expect(plan).toMatchObject( {
63+
"operation": "modify",
64+
"resourceType": "pip",
65+
"parameters": expect.arrayContaining([
66+
expect.objectContaining({
67+
"name": "install",
68+
"previousValue": [
69+
"ffmpeg",
70+
{
71+
"name": "qoverage",
72+
"version": "0.1.12"
73+
}
74+
],
75+
"newValue": [
76+
"ffmpeg",
77+
"ansible-roster",
78+
{
79+
"name": "qoverage",
80+
"version": "0.1.13"
81+
}
82+
],
83+
"operation": "modify"
84+
})
85+
])
86+
}
87+
)
88+
}
4889
}
49-
})
50-
})
90+
});
91+
});
5192
})

0 commit comments

Comments
 (0)