Skip to content

Commit a986ed4

Browse files
committed
Add git and bettercli
1 parent 1ce7304 commit a986ed4

File tree

8 files changed

+79
-14
lines changed

8 files changed

+79
-14
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ classifiers = [
2525
dependencies = [
2626
"fsspec>=2025.5.1",
2727
"pyyaml",
28-
"toml"
28+
"toml",
29+
"click"
2930
]
3031

3132
[project.optional-dependencies]

src/projspec/__main__.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
#!/usr/bin/env python
22
"""Simple example executable for this library"""
33

4-
import os
5-
import sys
4+
import click
65

76
import projspec.proj
87

98

10-
def main():
11-
if len(sys.argv) > 1:
12-
path = sys.argv[1]
9+
@click.command()
10+
@click.option(
11+
"--types",
12+
default="ALL",
13+
help='Type names to scan for (comma-separated list in camel or snake case); defaults to "ALL"',
14+
)
15+
@click.argument("path", default=".")
16+
def main(path, types):
17+
if types in {"ALL", ""}:
18+
types = None
1319
else:
14-
path = os.getcwd()
15-
proj = projspec.Project(path)
20+
types = types.split(",")
21+
proj = projspec.Project(path, types=types)
1622
print(proj)
1723

1824

src/projspec/content/vcs.py

Whitespace-only changes.

src/projspec/proj/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from .base import Project, ProjectSpec
22
from .conda_package import CondaRecipe, RattlerRecipe
33
from .conda_project import CondaProject
4+
from .git import GitRepo
45
from .pixi import Pixi
6+
from .poetry import PoetryProject
57
from .pyscript import PyScriptSpec
68
from .python_code import PythonCode, PythonLibrary
79
from .uv import UVProject
@@ -11,6 +13,8 @@
1113
"ProjectSpec",
1214
"CondaRecipe",
1315
"CondaProject",
16+
"GitRepo",
17+
"PoetryProject",
1418
"RattlerRecipe",
1519
"Pixi",
1620
"PyScriptSpec",

src/projspec/proj/base.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,47 @@ def __init__(
2424
storage_options: dict | None = None,
2525
fs: fsspec.AbstractFileSystem | None = None,
2626
walk: bool | None = None,
27+
types: set[str] | None = None,
2728
):
2829
if fs is None:
2930
fs, path = fsspec.url_to_fs(path, **(storage_options or {}))
3031
self.fs = fs
3132
self.url = path
3233
self.specs = {}
3334
self.children = {}
34-
self.resolve(walk=walk)
35+
self.resolve(walk=walk, types=types)
3536

3637
def is_local(self) -> bool:
3738
"""Did we read this from the local filesystem"""
3839
# see also fsspec.utils.can_be_local for more flexibility with caching.
3940
return isinstance(self.fs, fsspec.implementations.local.LocalFileSystem)
4041

41-
def resolve(self, subpath: str = "", walk: bool | None = None) -> None:
42+
def resolve(
43+
self,
44+
subpath: str = "",
45+
walk: bool | None = None,
46+
types: set[str] | None = None,
47+
) -> None:
4248
"""Fill out project specs in this directory
4349
4450
:param subpath: find specs at the given subpath
4551
:param walk: if None (default) only try subdirectories if root has
4652
no specs, and don't descend further. If True, recurse all directories;
4753
if False don't descend at all.
54+
:param types: names of types to allow while parsing. If empty or None, allow all
4855
"""
4956
fullpath = "/".join([self.url, subpath]) if subpath else self.url
5057
# sorting to ensure consistency
5158
for cls in sorted(registry, key=str):
5259
try:
5360
logger.debug("resolving %s as %s", fullpath, cls)
61+
name = cls.__name__
62+
snake_name = camel_to_snake(cls.__name__)
63+
if types and name not in types and snake_name not in types:
64+
continue
5465
inst = cls(self)
5566
inst.parse()
56-
self.specs[camel_to_snake(cls.__name__)] = inst
67+
self.specs[snake_name] = inst
5768
except ValueError:
5869
logger.debug("failed")
5970
except Exception as e:
@@ -71,6 +82,7 @@ def resolve(self, subpath: str = "", walk: bool | None = None) -> None:
7182
fileinfo["name"],
7283
fs=self.fs,
7384
walk=walk or False,
85+
types=types,
7486
)
7587
if proj2.specs:
7688
self.children[sub] = proj2
@@ -163,8 +175,8 @@ class ProjectSpec:
163175
def __init__(self, root: Project, subpath: str = ""):
164176
self.root = root
165177
self.subpath = subpath # not used yet
166-
self._contents = None
167-
self._artifacts = None
178+
self._contents = AttrDict()
179+
self._artifacts = AttrDict()
168180
if not self.match():
169181
raise ValueError(f"Not a {type(self).__name__}")
170182

src/projspec/proj/git.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from projspec.proj.base import ProjectSpec
2+
from projspec.utils import AttrDict
3+
4+
5+
class GitRepo(ProjectSpec):
6+
def match(self) -> bool:
7+
basenames = [_.rsplit("/", 1)[-1] for _ in self.root.filelist]
8+
return ".git" in basenames
9+
10+
def parse(self) -> None:
11+
cont = AttrDict()
12+
cont["remotes"] = [
13+
_.rsplit("/", 1)[-1]
14+
for _ in self.root.fs.ls(
15+
f"{self.root.url}/.git/refs/remotes", detail=False
16+
)
17+
]
18+
cont["tags"] = [
19+
_.rsplit("/", 1)[-1]
20+
for _ in self.root.fs.ls(
21+
f"{self.root.url}/.git/refs/tags", detail=False
22+
)
23+
]
24+
cont["branches"] = [
25+
_.rsplit("/", 1)[-1]
26+
for _ in self.root.fs.ls(
27+
f"{self.root.url}/.git/refs/heads", detail=False
28+
)
29+
]
30+
self._contents = cont

src/projspec/proj/poetry.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,13 @@ class PoetryProject(ProjectSpec):
55
spec_doc = "https://python-poetry.org/docs/pyproject/"
66

77
def match(self) -> bool:
8-
return "poetry" in self.root.metadata.get("tool")
8+
back = (
9+
self.root.pyproject.get("build_system", {})
10+
.get("build-backend", "")
11+
.startswith("poetry.")
12+
)
13+
return "poetry" in self.root.pyproject.get("tool") or back
14+
15+
def parse(self) -> None:
16+
# essentially the same as PythonLibrary?
17+
pass

src/projspec/proj/pyscript.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ class PyScriptSpec(ProjectSpec):
88
spec_doc = "https://docs.pyscript.net/2023.11.2/user-guide/configuration/"
99

1010
def match(self) -> bool:
11+
# actually, config can be specified by a local path in the repo, but this is rare;
12+
# also you can just declare things to install as you go, which we won't be able to
13+
# guess.
1114
basenames = [_.rsplit("/", 1)[-1] for _ in self.root.filelist]
1215
return "pyscript.toml" in basenames or "pyscript.json" in basenames
1316

0 commit comments

Comments
 (0)