Browse Source

more cleanup and fix "status" commands

shut-new-model
Niklas Rosenstein 9 months ago
parent
commit
d702d074bb
No known key found for this signature in database GPG Key ID: 6D269B33D25F6C6
12 changed files with 99 additions and 50 deletions
  1. + 2
    - 2
      src/shut/checks/package.py
  2. + 0
    - 13
      src/shut/commands/commons/__init__.py
  3. + 20
    - 14
      src/shut/commands/commons/status.py
  4. + 0
    - 5
      src/shut/commands/mono/__init__.py
  5. + 1
    - 1
      src/shut/commands/mono/checks.py
  6. + 6
    - 2
      src/shut/commands/mono/status.py
  7. + 2
    - 1
      src/shut/commands/pkg/status.py
  8. + 29
    - 6
      src/shut/model/__init__.py
  9. + 19
    - 1
      src/shut/model/abstract.py
  10. + 9
    - 0
      src/shut/model/monorepo.py
  11. + 10
    - 3
      src/shut/model/package.py
  12. + 1
    - 2
      src/shut/model/release.py

+ 2
- 2
src/shut/checks/package.py

@ -32,7 +32,7 @@ class PackageChecker(Checker[PackageModel]):
@check('readme')
def _check_readme(self, project: Project, package: PackageModel) -> Iterable[CheckResult]:
if package.get_readme():
if not package.get_readme_file():
yield CheckResult(CheckStatus.PASSED, 'No README file found.')
@check('license')
@ -40,7 +40,7 @@ class PackageChecker(Checker[PackageModel]):
if not package.data.license:
yield CheckResult(CheckStatus.WARNING, 'not specified')
elif package.data.license and not package.get_license():
elif package.data.license and not package.get_license_file():
yield CheckResult(CheckStatus.WARNING, 'No LICENSE file found.')
monorepo = project.monorepo

+ 0
- 13
src/shut/commands/commons/__init__.py

@ -18,16 +18,3 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from shore.model import ObjectCache
import os
object_cache = ObjectCache()
def load_manifest(filename_choices, cls):
filename_choices = tuple(filename_choices)
for filename in filename_choices:
if os.path.isfile(filename):
return cls.load(filename, object_cache)
raise RuntimeError('file not found: ' + repr(filename_choices))

+ 20
- 14
src/shut/commands/commons/status.py

@ -19,38 +19,44 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import os
from typing import Union
from nr.utils.git import Git
from shore.model import Monorepo, Package
from termcolor import colored
from typing import Union
from shut.model import AbstractProjectModel, MonorepoModel, Project
def get_commits_since_last_tag(subject: Union[Monorepo, Package]):
tag = subject.get_tag(subject.version)
def get_commits_since_last_tag(subject: AbstractProjectModel):
tag = subject.get_tag(subject.get_version())
ref = Git().rev_parse(tag)
if not ref:
return tag, None
else:
return tag, len(Git().rev_list(tag + '..HEAD', subject.directory))
return tag, len(Git().rev_list(tag + '..HEAD', subject.get_directory()))
def print_status(subject: Union[Monorepo, Package]) -> None:
def print_status(project: Project) -> None:
"""
The latest version is taken from the current version number in the package
configuration file. Git is then queried for the tag and the commit distance
to the current revision.
"""
items = [subject]
assert project.subject, "No subject"
if isinstance(subject, Monorepo):
items.extend(sorted(subject.get_packages(), key=lambda x: x.name))
if not subject.version:
items.remove(subject)
if isinstance(project.subject, MonorepoModel):
monorepo_dir = project.subject.get_directory()
items = sorted(project.packages, key=lambda x: x.data.name)
names = [os.path.normpath(os.path.relpath(x.get_directory(), monorepo_dir)) for x in items]
else:
items = [project.subject]
names = [project.subject.get_name()]
width = max(len(x.local_name) for x in items)
width = max(map(len, names))
for item in items:
for item, name in zip(items, names):
tag, num_commits = get_commits_since_last_tag(item)
if num_commits is None:
item_info = colored('tag "{}" not found'.format(tag), 'red')
@ -58,4 +64,4 @@ def print_status(subject: Union[Monorepo, Package]) -> None:
item_info = colored('no commits', 'green') + ' since "{}"'.format(tag)
else:
item_info = colored('{} commit(s)'.format(num_commits), 'yellow') + ' since "{}"'.format(tag)
print('{}: {}'.format(item.local_name.rjust(width), item_info))
print('{}: {}'.format(name.rjust(width), item_info))

+ 0
- 5
src/shut/commands/mono/__init__.py

@ -20,7 +20,6 @@
# IN THE SOFTWARE.
from .. import shut, commons
from shore.model import Monorepo
import click
@ -31,10 +30,6 @@ def mono():
"""
def load_monorepo_manifest() -> Monorepo:
return commons.load_manifest(('monorepo.yaml', 'monorepo.yml'), Monorepo)
from . import checks
from . import new
from . import status

+ 1
- 1
src/shut/commands/mono/checks.py

@ -50,7 +50,7 @@ def checks(warnings_as_errors):
"""
start_time = time.perf_counter()
monorepo = project.load(expect=MonorepoModel)
monorepo = project.load_or_exit(expect=MonorepoModel)
checks = sorted(get_checks(project, monorepo), key=lambda c: c.name)
seconds = time.perf_counter() - start_time
print_checks_all(monorepo.name, checks, seconds)

+ 6
- 2
src/shut/commands/mono/status.py

@ -19,12 +19,16 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
from . import mono, load_monorepo_manifest
from . import mono
from .. import project
from ..commons.status import print_status
from shut.model.monorepo import MonorepoModel
@mono.command(help="""
Show which packages have been modified since their last release.
""" + print_status.__doc__)
def status():
print_status(load_monorepo_manifest())
project.load_or_exit(expect=MonorepoModel)
print_status(project)

+ 2
- 1
src/shut/commands/pkg/status.py

@ -29,4 +29,5 @@ from shut.model import PackageModel
Shows whether the package was modified since the last release.
""" + print_status.__doc__)
def status():
print_status(project.load(expect=PackageModel))
project.load_or_exit(expect=PackageModel)
print_status(project)

+ 29
- 6
src/shut/model/__init__.py

@ -32,6 +32,7 @@ registry = Registry(json_registry)
registry.set_option(datamodel, 'skip_defaults', True)
ExcInfo = Tuple
from .abstract import AbstractProjectModel
from .monorepo import MonorepoModel
from .package import PackageModel
@ -44,6 +45,16 @@ def get_existing_file(directory: str, choices: List[str]) -> bool:
return None
class Unexpected(Exception):
def __init__(self, expected, got):
self.expected = expected
self.got = got
def __str__(self):
return f'expected: {self.expected}, got {self.got}'
class Project:
"""
Loads package and mono repo configuration files and caches them to ensure that
@ -53,9 +64,11 @@ class Project:
monorepo_filenames = ['monorepo.yml', 'monorepo.yaml']
package_filenames = ['package.yml', 'package.yaml']
Unexpected = Unexpected
def __init__(self):
self._cache: Dict[str, Union[MonorepoModel, PackageModel]] = {}
self.subject: Union[MonorepoModel, PackageModel] = None
self._cache: Dict[str, AbstractProjectModel] = {}
self.subject: AbstractProjectModel = None
self.monorepo: MonorepoModel = None
self.packages: List[PackageModel] = []
self.invalid_packages: List[Tuple[str, ExcInfo]] = []
@ -63,8 +76,8 @@ class Project:
def load(
self,
directory: str = '.',
expect: Type[Union[MonorepoModel, PackageModel]] = None,
) -> Union[MonorepoModel, PackageModel]:
expect: Type[AbstractProjectModel] = None,
) -> AbstractProjectModel:
"""
Loads all project information from *directory*. This searches in all parent directories
for a package or monorepo configuration, then loads all resources that belong to the
@ -93,11 +106,21 @@ class Project:
self.subject = self._load_package(package_fn)
if expect and not isinstance(self.subject, expect):
raise TypeError('expected {!r} at {!r}, got {!r}'.format(
expect.__name__, directory, type(self.subject).__name__))
raise Unexpected(expect, type(self.subject))
return self.subject
def load_or_exit(self, *args, **kwargs):
try:
return self.load(*args, **kwargs)
except Unexpected as exc:
if exc.expected == MonorepoModel:
sys.exit('error: not in a mono repository context')
elif exc.expected == PackageModel:
sys.exit('error: not in a package context')
else:
raise
def _load_object(self, filename: str, type_: Type[T]) -> T:
filename = os.path.normpath(os.path.abspath(filename))
if filename in self._cache:

+ 19
- 1
src/shut/model/abstract.py

@ -19,17 +19,35 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
import abc
import os
from typing import List, Optional
from databind.core import datamodel, field
from .changelog import ChangelogConfiguration
from .release import ReleaseConfiguration
from .version import Version
@datamodel
class AbstractProjectModel:
class AbstractProjectModel(metaclass=abc.ABCMeta):
filename: Optional[str] = field(derived=True, default=None)
unknown_keys: List[str] = field(derived=True, default_factory=list)
changelog: ChangelogConfiguration = field(default_factory=ChangelogConfiguration)
release: ReleaseConfiguration = field(default_factory=ReleaseConfiguration)
@abc.abstractmethod
def get_name(self) -> str:
pass
@abc.abstractmethod
def get_version(self) -> Optional[Version]:
pass
def get_tag(self, version: Version) -> str:
return self.release.tag_format.format(name=self.get_name(), version=version)
def get_directory(self) -> str:
return os.path.dirname(self.filename)
def get_changelog_directory(self) -> str:
return os.path.join(os.path.dirname(self.filename), self.changelog.directory)

+ 9
- 0
src/shut/model/monorepo.py

@ -34,4 +34,13 @@ class MonorepoModel(AbstractProjectModel):
author: Optional[Author] = None
license: str = None
url: str = None
# Overrides
release: MonorepoReleaseConfiguration = field(default_factory=MonorepoReleaseConfiguration)
def get_name(self) -> str:
return self.name
def get_version(self) -> Optional[Version]:
return self.version

+ 10
- 3
src/shut/model/package.py

@ -98,7 +98,6 @@ class PackageModel(AbstractProjectModel):
data: PackageData = field(altname='package')
install: InstallConfiguration = field(default_factory=InstallConfiguration)
linter: LinterConfiguration = field(default_factory=LinterConfiguration)
release: ReleaseConfiguration = field(default_factory=ReleaseConfiguration)
def get_python_package_metadata(self) -> 'PythonPackageMetadata':
"""
@ -111,7 +110,7 @@ class PackageModel(AbstractProjectModel):
os.path.join(os.path.dirname(self.filename), self.data.source_directory),
self.data.get_modulename())
def get_readme(self) -> Optional[str]:
def get_readme_file(self) -> Optional[str]:
"""
Returns the absolute path to the README for this package.
"""
@ -126,7 +125,7 @@ class PackageModel(AbstractProjectModel):
prefix='README.',
preferred=['README.md', 'README.rst', 'README.txt', 'README'])
def get_license(self) -> Optional[str]:
def get_license_file(self) -> Optional[str]:
"""
Returns the absolute path to the LICENSE file for this package.
"""
@ -136,6 +135,14 @@ class PackageModel(AbstractProjectModel):
prefix='LICENSE.',
preferred=['LICENSE', 'LICENSE.txt', 'LICENSE.rst', 'LICENSE.md'])
# AbstractProjectModel
def get_name(self) -> str:
return self.data.name
def get_version(self) -> Optional[Version]:
return self.data.version
class PythonPackageMetadata:
"""

+ 1
- 2
src/shut/model/release.py

@ -24,10 +24,9 @@ from databind.core import datamodel, field
@datamodel
class ReleaseConfiguration:
private: bool = False
tag_format: str = field(altname='tag-format', default='{version}')
@datamodel
class MonorepoReleaseConfiguration:
class MonorepoReleaseConfiguration(ReleaseConfiguration):
single_version: bool = field(altname='single-version', default=False)

Loading…
Cancel
Save