Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/source/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,56 @@ Adding docs
Docstrings, prose text and examples/tutorials are eagerly accepted! We, as coders, often
are late to fully document our work, and all contributions are welcome. Separate instructions
can be found in the docs/README.md file.

Adding a parser
===============

The main job of ``projspec`` is to interpret project metadat files into the component
"content" and "artifact" classes a project contains, for a given spec. This job is done
by parsers, each subclasses of :class:`projspec.proj.base.ProjectSpec`.

All subclasses are added to the registry on import, and when constructing a
:class:`projspec.proj.base.Project`, each of these classes attempts to parse the
target directory. Any specs that succeed in parsing will populate the `Project`'s
`.specs` dictionary.

.. note::

``projspec`` will eventually have a config system to be able to import ProjectSpec
subclasses from other packages. For now, any new parsers added in this repo should also
be imported in the package ``__init__.py`` file, so that they will appear in the registry.

Only two methods need to be implemented:

* ``.match()``, which answers whether this directory *might* be interpretable as the given
project type. If returning ``True``, the ``.parse()`` method will be attempted. The check
here should be constant time and fast. Most typically it will depend on the existence of
some known file in the directory root or entries in the pyproject metadata
(``.filelist``, ``.basenames`` and ``.pyproject`` are sll cached attributes of ``self.proj``).

* ``.parse()``, which populates the ``._contents`` and ``._artifacts`` attributes with
instances of subclasses of :class:`projspec.content.base.BaseContent` and
:class:`projspec.artifact.base.BaseArtifact`, respectively. In a minority of cases,
simple-typed values might suffice, for example the tags in a git repo are just strings
without further details.

``parse()`` should raise ``ValueError`` if parsing fails, which will cause the
corresponding project spec type not to show up in the enclosing ``Project`` instance.

This typically involves reading some metadata file, and constructing the instances. The
attributes are instances of ``projspec.utils.AttrDict``, which behaves like a dict for
assignment. The convention is, that keynames should be the "snake name" version of the
class, and the values are either a single instance, a list of instances, or a dict of
named instance. An example of the latter might be named environments:

.. code-block:: python

{"environment": {"default": Environment()}}

Sometimes, new Content and Artifact classes will be required too.

The special case of :class:`projspec.proj.base.ProjectExtra` exists for specs where the
content/artifact is part of the overall project, but doesn't really make sense as a project
by itself. For instance, a Dockerfile will make use of the files in a directory to
create a docker image (an Artifact of the project), but in most cases that does not make
the directory a "Docker project".
20 changes: 19 additions & 1 deletion src/projspec/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"""Simple example executable for this library"""

import json
import pydoc
import sys

import click

Expand All @@ -27,16 +29,32 @@
@click.option(
"--make", help="(Re)Create the first artifact found matching this type name"
)
@click.option(
"--info",
help="Give information about a names entity type (spec, contents or artifact)",
)
@click.option(
"--storage_options",
default="",
help="storage options dict for the given URL, as JSON",
)
def main(path, types, xtypes, walk, summary, make, storage_options):
def main(path, types, xtypes, walk, summary, make, info, storage_options):
if types in {"ALL", ""}:
types = None
else:
types = types.split(",")
if info:
info = projspec.utils.camel_to_snake(info)
cls = (
projspec.proj.base.registry.get(info)
or projspec.content.base.registry.get(info)
or projspec.artifact.base.registry.get(info)
)
if cls:
pydoc.doc(cls, output=sys.stdout)
else:
print("Name not found")
return
if xtypes in {"NONE", ""}:
xtypes = None
else:
Expand Down
11 changes: 10 additions & 1 deletion src/projspec/artifact/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@


class BaseArtifact:
"""A thing that a project can o or make

Artifacts are the "actions" of a project spec. Most typically, they involve
calling the external tool associated with the project type in a subprocess.
"""

def __init__(self, proj: Project, cmd: list[str] | None = None):
self.proj = proj
self.cmd = cmd
Expand Down Expand Up @@ -98,7 +104,10 @@ def get_cls(name: str) -> type[BaseArtifact]:


class FileArtifact(BaseArtifact):
"""Specialised artifacts, where the output is one or more files"""
"""Specialised artifacts, where the output is one or more files

Ideally, we can know beforehand the path expected for the output.
"""

# TODO: account for outputs to a directory/glob pattern, so we can
# apply to wheel; or unknown output location, e.g., conda-build.
Expand Down
8 changes: 8 additions & 0 deletions src/projspec/content/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@

@dataclass
class BaseContent:
"""A descriptive piece of information declared in a project

Content classes tell you something fundamental about a project, but do
not have any other functionality than to allow introspection. We use
dataclasses to define what information a given Content subclass should
provide.
"""

proj: Project = field(repr=False)
artifacts: set[BaseArtifact] = field(repr=False)

Expand Down
2 changes: 2 additions & 0 deletions src/projspec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ class IsInstalled:

Results are cached by command and python executable, so that in the
future we may be able to persist these for future sessions.

An instance of this class is created at import: ``projspec.utils.is_installed``.
"""

cache = {}
Expand Down