Skip to content

Commit 272b68c

Browse files
committed
a little docker fun
1 parent 611815f commit 272b68c

File tree

9 files changed

+107
-14
lines changed

9 files changed

+107
-14
lines changed

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM python
2+
3+
# todo: exclude potentially large env directories
4+
COPY . .
5+
RUN pip install . --no-cache-dir
6+
CMD ["projspec"]

src/projspec/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def main(path, types, xtypes, walk, summary, make, storage_options):
5050
)
5151
if make:
5252
art: projspec.artifact.BaseArtifact
53-
for art in proj.artifacts:
53+
for art in proj.all_artifacts():
5454
if art.snake_name() == projspec.utils.camel_to_snake(make):
5555
print("Launching:", art)
5656
art.remake()

src/projspec/artifact/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
"""Things that a project can do or make"""
22

33
from projspec.artifact.base import BaseArtifact, FileArtifact
4+
from projspec.artifact.container import DockerImage
45
from projspec.artifact.installable import CondaPackage, Wheel
56
from projspec.artifact.process import Process
67
from projspec.artifact.python_env import EnvPack, CondaEnv, VirtualEnv, LockFile
78

89
__all__ = [
910
"BaseArtifact",
1011
"FileArtifact",
12+
"DockerImage",
1113
"CondaPackage",
1214
"Wheel",
1315
"Process",

src/projspec/artifact/container.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import subprocess
2+
3+
from projspec.proj.base import Project, ProjectExtra
4+
from projspec.artifact import BaseArtifact
5+
6+
7+
class DockerImage(BaseArtifact):
8+
def __init__(self, proj: Project, cmd=None, tag=None):
9+
if tag:
10+
cmd = ["docker", "build", ".", "-t", tag]
11+
else:
12+
cmd = ["docker", "build", "."]
13+
self.tag = tag
14+
super().__init__(proj, cmd=cmd)
15+
16+
17+
class DockerRuntime(DockerImage):
18+
# Note: there are many optional arguments to docker; we could surface the most common
19+
# ones (-it, -d, -p). This does the simplest thing.
20+
21+
def _make(self, *args, **kwargs) -> None:
22+
"""
23+
24+
:param args: added to the docker run command
25+
:param kwargs: affect the docker run subprocess call
26+
"""
27+
out = subprocess.check_output(self.cmd, cwd=self.proj.url, **kwargs)
28+
if self.tag:
29+
subprocess.check_call(["docker", "run", self.tag])
30+
else:
31+
lines = [
32+
l for l in out.splitlines() if l.startswith(b"Successfully built ")
33+
]
34+
img = lines[-1].split()[-1]
35+
subprocess.check_call(
36+
["docker", "run", img.decode()] + list(args), **kwargs
37+
)
38+
39+
40+
class Docker(ProjectExtra):
41+
def match(self):
42+
return "Dockerfile" in self.proj.basenames
43+
44+
def parse(self) -> None:
45+
self._artifacts["docker_image"] = DockerImage(self.proj)
46+
self._artifacts["docker_runtime"] = DockerRuntime(self.proj)

src/projspec/content/cicd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55

66
class GithubAction(BaseContent):
7-
"""Run prescription that runs in github on push/merge"""
7+
"""A run prescription that runs in github on push/merge"""
88

99
# TODO: we probably want to extract out the jobs and runs, maybe the steps.
1010
# It may be interesting to provide links to the browser or API to view

src/projspec/proj/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from projspec.proj.conda_project import CondaProject
44
from projspec.proj.documentation import RTD, MDBook
55
from projspec.proj.git import GitRepo
6+
from projspec.proj.ide import JetbrainsIDE, NvidiaAIWorkbench, VSCode
67
from projspec.proj.pixi import Pixi
78
from projspec.proj.poetry import Poetry
89
from projspec.proj.pyscript import PyScript
@@ -17,7 +18,9 @@
1718
"CondaRecipe",
1819
"CondaProject",
1920
"GitRepo",
21+
"JetbrainsIDE",
2022
"MDBook",
23+
"NvidiaAIWorkbench",
2124
"Poetry",
2225
"RattlerRecipe",
2326
"Pixi",
@@ -28,4 +31,5 @@
2831
"Rust",
2932
"RustPython",
3033
"Uv",
34+
"VSCode",
3135
]

src/projspec/proj/base.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from collections.abc import Iterable
3+
from itertools import chain
34
from functools import cached_property
45

56
import fsspec
@@ -61,6 +62,8 @@ def __init__(
6162
self.url = path
6263
self.specs = AttrDict()
6364
self.children = AttrDict()
65+
self.contents = AttrDict()
66+
self.artifacts = AttrDict()
6467
self.excludes = excludes or default_excludes
6568
self._pyproject = None
6669
self.resolve(walk=walk, types=types, xtypes=xtypes)
@@ -90,17 +93,21 @@ def resolve(
9093
for name in sorted(registry):
9194
cls = registry[name]
9295
try:
93-
logger.debug("resolving %s as %s", fullpath, cls)
9496
name = cls.__name__
9597
snake_name = camel_to_snake(cls.__name__)
9698
if (types and {name, snake_name}.isdisjoint(types)) or {
9799
name,
98100
snake_name,
99101
}.intersection(xtypes or set()):
100102
continue
103+
logger.debug("resolving %s as %s", fullpath, cls)
101104
inst = cls(self)
102105
inst.parse()
103-
self.specs[snake_name] = inst
106+
if isinstance(inst, ProjectExtra):
107+
self.contents.update(inst.contents)
108+
self.artifacts.update(inst.artifacts)
109+
else:
110+
self.specs[snake_name] = inst
104111
except ParseFailed:
105112
logger.debug("failed")
106113
except Exception as e:
@@ -148,7 +155,7 @@ def text_summary(self) -> str:
148155
"""Only shows project types, not what they contain"""
149156
txt = f"<Project '{self.fs.unstrip_protocol(self.url)}'>\n"
150157
bits = [
151-
f" {'/'}: {' '.join(type(_).__name__ for _ in self.specs.values())}"
158+
f" {'/'}: {' '.join(type(_).__name__ for _ in chain(self.specs.values(), self.contents.values(), self.artifacts.values()))}"
152159
] + [
153160
f" {k}: {' '.join(type(_).__name__ for _ in v.specs.values())}"
154161
for k, v in self.children.items()
@@ -160,6 +167,12 @@ def __repr__(self):
160167
self.fs.unstrip_protocol(self.url),
161168
"\n\n".join(str(_) for _ in self.specs.values()),
162169
)
170+
if self.contents:
171+
ch = "\n".join([f" {k}: {v}" for k, v in self.contents.items()])
172+
txt += f"\nContents:\n{ch}"
173+
if self.artifacts:
174+
ch = "\n".join([f" {k}: {v}" for k, v in self.artifacts.items()])
175+
txt += f"\nArtifacts:\n{ch}"
163176
if self.children:
164177
ch = "\n".join(
165178
[
@@ -194,23 +207,32 @@ def pyproject(self):
194207
pass
195208
return {}
196209

197-
@property
198-
def artifacts(self) -> set:
210+
def all_artifacts(self, names=None) -> list:
199211
"""A flat list of all the artifact objects nested in this project."""
200-
arts = set()
212+
arts = set(self.artifacts.values())
201213
for spec in self.specs.values():
202214
arts.update(flatten(spec.artifacts))
203215
for child in self.children.values():
204216
arts.update(child.artifacts)
217+
if names:
218+
if isinstance(names, str):
219+
names = {names}
220+
arts = [
221+
a
222+
for a in arts
223+
if any(a.snake_name() == camel_to_snake(n) for n in names)
224+
]
225+
else:
226+
arts = list(arts)
205227
return arts
206228

207-
def filter_by_type(self, types: Iterable[type]) -> bool:
229+
def has_artifact_type(self, types: Iterable[type]) -> bool:
208230
"""Answers 'does this project support outputting the given artifact type'
209231
210232
This is an experimental example of filtering through projects
211233
"""
212234
types = tuple(types)
213-
return any(isinstance(_, types) for _ in self.artifacts)
235+
return any(isinstance(_, types) for _ in self.all_artifacts())
214236

215237
def __contains__(self, item) -> bool:
216238
"""Is the given project type supported ANYWHERE in this directory?"""
@@ -222,6 +244,8 @@ def to_dict(self, compact=True) -> dict:
222244
children=self.children,
223245
url=self.url,
224246
storage_options=self.storage_options,
247+
artifacts=self.artifacts,
248+
contents=self.contents,
225249
)
226250
if not compact:
227251
dic["klass"] = "project"
@@ -346,11 +370,13 @@ def snake_name(cls) -> str:
346370
class ProjectExtra(ProjectSpec):
347371
"""A special subcategory of project types with content but no structure.
348372
349-
Subclasses of this are special, in that they are not free-standing projects, but add
373+
Subclasses of this are special: they are not free standing projects, but add
350374
contents onto the root project. Examples include data catalog specification, linters
351-
and Ci/CD, that may be run against the root project without using a project-oriented
375+
and CI/CD, that may be run against the root project without using a project-oriented
352376
tool.
353377
354378
These classes do not appear in a Project's .specs, but do contribute .contents or
355379
.artifacts. They are still referenced when filtering spec types by name.
380+
381+
Commonly, subclasses are tied 1-1 to a particular content/artifact class.
356382
"""

src/projspec/proj/ide.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ class NvidiaAIWorkbench(ProjectSpec):
1111
def match(self) -> bool:
1212
return self.proj.fs.exists(f"{self.proj.url}/.project/spec.yaml")
1313

14+
def parse(self) -> None:
15+
...
16+
1417

1518
class JetbrainsIDE(ProjectSpec):
1619
def match(self) -> bool:
@@ -25,9 +28,15 @@ class VSCode(ProjectSpec):
2528
def match(self) -> bool:
2629
return self.proj.fs.exists(f"{self.proj.url}/.vscode/settings.json")
2730

31+
def parse(self) -> None:
32+
...
33+
2834

2935
class Zed(ProjectSpec):
3036
spec_doc = "https://zed.dev/docs/configuring-zed#settings"
3137

3238
def match(self) -> bool:
3339
return self.proj.fs.exists(f"{self.proj.url}/.zed/settings.json")
40+
41+
def parse(self) -> None:
42+
...

tests/test_basic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
def test_basic(proj):
1010
spec = proj.specs["python_library"]
1111
assert "wheel" in spec.artifacts
12-
assert proj.artifacts
12+
assert proj.all_artifacts()
1313
assert proj.children
1414
repr(proj)
1515
proj._repr_html_()
@@ -25,7 +25,7 @@ def test_contains(proj):
2525

2626
assert proj.python_library is not None
2727
assert "python_library" in proj
28-
assert proj.filter_by_type([Wheel])
28+
assert proj.has_artifact_type([Wheel])
2929

3030

3131
def test_serialise(proj):

0 commit comments

Comments
 (0)