diff --git a/pyproject.toml b/pyproject.toml index 9ab0dd3..6761b55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,8 @@ dependencies = [ ] [project.optional-dependencies] -test = ["pytest", "pytest-cov", "django", "streamlit", "copier", "jinja2-time", "flask"] +test = ["pytest", "pytest-cov", "django", "streamlit", "copier", "jinja2-time", "flask", + "maturin"] qt = ["pyqt>5,<6", "pyqtwebengin>5,<6"] [project.scripts] diff --git a/src/projspec/__main__.py b/src/projspec/__main__.py index 796dcb3..61ffea0 100755 --- a/src/projspec/__main__.py +++ b/src/projspec/__main__.py @@ -51,7 +51,7 @@ def make(artifact, path, storage_options, types, xtypes): proj = projspec.Project( path, storage_options=storage_options, types=types, xtypes=xtypes ) - proj.make(artifact) + print("Created:", proj.make(artifact)) @main.command() diff --git a/src/projspec/proj/base.py b/src/projspec/proj/base.py index f94b57f..26e22ca 100644 --- a/src/projspec/proj/base.py +++ b/src/projspec/proj/base.py @@ -83,9 +83,16 @@ def __init__( self.artifacts = AttrDict() # read and respect .gitignore? for exclude directories? self.excludes = excludes or default_excludes - self._pyproject = None - self._scanned_files = None + self._reset() self.resolve(walk=walk, types=types, xtypes=xtypes) + + def _reset(self): + """Prepare this project for parsing with new specs""" + # cached properties + self.__dict__.pop("basenames", None) + self.__dict__.pop("filelist", None) + self.__dict__.pop("pyproject", None) + self._scanned_files = None # clear cached files self._scanned_files = None @@ -368,12 +375,22 @@ def from_dict(dic): proj.fs, proj.url = fsspec.url_to_fs(proj.path, **proj.storage_options) return proj - def create(self, name: str): - """Make this project conform to the given project spec type.""" + def create(self, name: str) -> list[str]: + """Make this project conform to the given project spec type. + + Returns a list of files that were created. + """ cls = get_cls(name) # causes reparse and makes a new instance # could rerun resolve or only parse for give type and add, instead. - return cls.create(self.path) + allfiles = self.fs.find(self.url, detail=False) + cls.create(self.url) + allfiles2 = self.fs.find(self.url, detasil=False) + self._reset() + spec = cls(self) + spec.parse() + self.specs[camel_to_snake(cls.__name__)] = spec + return sorted(set(allfiles2) - set(allfiles)) def make(self, qname: str, **kwargs) -> None: """Make an artifact of the given type @@ -467,7 +484,7 @@ def _create(path: str) -> None: raise NotImplementedError("Subclass must implement this") @classmethod - def create(cls, path: str) -> Project: + def create(cls, path: str): """Make the target directory compliant with this project type, if not already""" # TODO: implement remote?? # TODO: implement dry-run? @@ -476,8 +493,6 @@ def create(cls, path: str) -> Project: os.makedirs(path, exist_ok=True) if not cls.snake_name() in Project(path): cls._create(path) - # perhaps should return ProjSpec, but it needs to be added to a project - return Project(path) def parse(self) -> None: raise ParseFailed diff --git a/src/projspec/proj/rust.py b/src/projspec/proj/rust.py index 48b942f..24a4631 100644 --- a/src/projspec/proj/rust.py +++ b/src/projspec/proj/rust.py @@ -1,5 +1,8 @@ +import subprocess + import toml from projspec.proj import ProjectSpec, PythonLibrary +from projspec.utils import AttrDict class Rust(ProjectSpec): @@ -12,12 +15,31 @@ def match(self) -> bool: def parse(self): from projspec.content.metadata import DescriptiveMetadata + from projspec.artifact.base import FileArtifact with self.proj.fs.open(f"{self.proj.url}/Cargo.toml", "rt") as f: meta = toml.load(f) self.contents["desciptive_metadata"] = DescriptiveMetadata( proj=self.proj, meta=meta.get("package") ) + bin = AttrDict() + bin["debug"] = FileArtifact( + proj=self.proj, + cmd=["cargo", "build"], + # extension is platform specific + fn=f"{self.proj.url}/target/debug/{meta['package']['name']}.*", + ) + bin["release"] = FileArtifact( + proj=self.proj, + cmd=["cargo", "build", "--release"], + # extension is platform specific + fn=f"{self.proj.url}/target/release/{meta['package']['name']}.*", + ) + self.artifacts["file"] = bin + + @staticmethod + def _create(path: str) -> None: + subprocess.check_call(["cargo", "init"], cwd=path) class RustPython(Rust, PythonLibrary): @@ -33,10 +55,18 @@ def match(self) -> bool: # have a python package directory with the same name as the rust library. # You can also have metadata.maturin in the Cargo.toml - return ( - Rust.match(self) - and "maturin" in self.proj.pyproject.get("tool", {}) - and self.proj.pyproject.get("build-backend", "") == "maturin" + return Rust.match(self) and ( + "maturin" in self.proj.pyproject.get("tool", {}) + or self.proj.pyproject.get("build-system", {}).get("build-backend", "") + == "maturin" ) - # this builds a python-installable wheel in addition to rust artifacts. + def parse(self): + super().parse() + Rust.parse(self) + + @staticmethod + def _create(path: str) -> None: + # will fail for existing python libraries, since it doesn't want to edit + # the pyproject.toml build backend. + subprocess.check_call(["maturin", "init", "-b", "pyo3", "--mixed"], cwd=path) diff --git a/tests/test_roundtrips.py b/tests/test_roundtrips.py index 8ea51d0..676fb29 100644 --- a/tests/test_roundtrips.py +++ b/tests/test_roundtrips.py @@ -1,3 +1,4 @@ +import os.path import pytest import projspec.proj @@ -24,12 +25,16 @@ "HuggingFaceRepo", "uv_script", "MLFlow", + "Rust", + "RustPython", ], ) def test_compliant(tmpdir, cls_name): path = str(tmpdir) cls = get_cls(cls_name) - proj = cls.create(path) + proj = projspec.Project(path) + files = proj.create(cls_name) + assert os.path.exists(files[0]) if not issubclass(cls, projspec.proj.ProjectExtra): assert cls_name in proj else: diff --git a/tests/test_webapp.py b/tests/test_webapp.py index e926a86..7f8209a 100644 --- a/tests/test_webapp.py +++ b/tests/test_webapp.py @@ -3,8 +3,8 @@ def test_webapp_kwargs(tmpdir): proj = projspec.Project(str(tmpdir)) - proj2 = proj.create("flask") - art = proj2.flask.artifacts["server"]["flask-app"] + proj.create("flask") + art = proj.flask.artifacts["server"]["flask-app"] art.make() # defaults for flask assert art._port == 5000