Skip to content

Commit

Permalink
feat: typing as per PEP-561 and other refactors (#114)
Browse files Browse the repository at this point in the history
* feat: typing of `jake` WIP

Signed-off-by: Paul Horton <phorton@sonatype.com>

* port of fix for #112

Signed-off-by: Paul Horton <phorton@sonatype.com>

* resolved a bunch of typing issues

Signed-off-by: Paul Horton <phorton@sonatype.com>

* ci: fixed parameter references

Signed-off-by: Paul Horton <phorton@sonatype.com>

* ci: fixed parameter references

Signed-off-by: Paul Horton <phorton@sonatype.com>

* ci: fixed mypy

Signed-off-by: Paul Horton <phorton@sonatype.com>

* fix: updated `ossindex-lib` to latest RC which now appears to properly resolve caching issues #100

Signed-off-by: Paul Horton <phorton@sonatype.com>

* defined lowest dependencies and aligned

Signed-off-by: Paul Horton <phorton@sonatype.com>

* defined lowest dependencies and aligned

Signed-off-by: Paul Horton <phorton@sonatype.com>

* defined lowest dependencies and aligned

Signed-off-by: Paul Horton <phorton@sonatype.com>

* defined lowest dependencies and aligned

Signed-off-by: Paul Horton <phorton@sonatype.com>

* WIP: JSON output updated to use JSON serialisation
- All tests passing locally

Signed-off-by: Paul Horton <phorton@sonatype.com>
  • Loading branch information
madpah committed Feb 21, 2022
1 parent 223978f commit a0ab7ee
Show file tree
Hide file tree
Showing 14 changed files with 537 additions and 384 deletions.
30 changes: 26 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ jobs:
- checkout
- restore_cache: # ensure this step occurs *before* installing dependencies
name: "Restore any valid cache"
key: dependencies-{{ .Branch }}-{{ parameters.python_version }}-{{ checksum "poetry.lock" }}
key: dependencies-{{ .Branch }}-<< parameters.python_version >>-{{ checksum "poetry.lock" }}
- run:
command: |
poetry install
- save_cache:
name: "Cache dependencies"
key: dependencies-{{ .Branch }}-{{ parameters.python_version }}-{{ checksum "poetry.lock" }}
key: dependencies-{{ .Branch }}-<< parameters.python_version >>-{{ checksum "poetry.lock" }}
paths:
- /home/circleci/.cache/pypoetry/virtualenvs
- run:
Expand All @@ -84,13 +84,13 @@ jobs:
- checkout
- restore_cache: # ensure this step occurs *before* installing dependencies
name: "Restore any valid cache"
key: dependencies-{{ .Branch }}-{{ parameters.python_version }}-{{ checksum "poetry.lock" }}
key: dependencies-{{ .Branch }}-310-{{ checksum "poetry.lock" }}
- run:
command: |
poetry install
- save_cache:
name: "Cache dependencies"
key: dependencies-{{ .Branch }}-{{ parameters.python_version }}-{{ checksum "poetry.lock" }}
key: dependencies-{{ .Branch }}-310-{{ checksum "poetry.lock" }}
paths:
- /home/circleci/.cache/pypoetry/virtualenvs
- run:
Expand Down Expand Up @@ -124,6 +124,23 @@ jobs:
git config user.email "$GITHUB_EMAIL"
semantic-release publish
static_code_analysis:
parameters:
python_version:
type: string
toxenv_factor:
type: string
executor: python<< parameters.python_version >>
steps:
- checkout
- ensure_poetry_installed
- run:
command: |
poetry install --no-root
- run:
command: |
poetry run tox -e mypy-<< parameters.toxenv_factor >>
workflows:
cicd:
jobs:
Expand All @@ -132,6 +149,11 @@ workflows:
parameters:
python_version: ["310", "39", "38", "37", "36"]
- coding_standards
- static_code_analysis:
matrix:
parameters:
python_version: [ "310", "36" ]
toxenv_factor: [ "locked", "lowest" ]
release:
jobs:
- manual_release:
Expand Down
51 changes: 51 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#
# Copyright 2019-Present Sonatype Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
[mypy]

files = jake/

show_error_codes = True
pretty = True

warn_unreachable = True
allow_redefinition = False

# ignore_missing_imports = False
# follow_imports = normal
# follow_imports_for_stubs = True

### Strict mode ###
warn_unused_configs = True
disallow_subclassing_any = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_return_any = True
no_implicit_reexport = True

# needed to silence some py37|py38 differences
warn_unused_ignores = False

[mypy-pytest.*]
ignore_missing_imports = True

[mypy-tests.*]
disallow_untyped_decorators = False
110 changes: 46 additions & 64 deletions jake/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,112 +16,94 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
from argparse import ArgumentParser
from datetime import datetime
from typing import Dict

from pyfiglet import figlet_format
from pyfiglet import figlet_format # type: ignore
from rich.console import Console

from .command import BaseCommand, _jake_version
from .command import BaseCommand, jake_version
from .command.iq import IqCommand
from .command.oss import OssCommand
from .command.sbom import SbomCommand

_SUB_COMMANDS: Dict[str, BaseCommand] = {
'iq': IqCommand(),
'ddt': OssCommand(),
'sbom': SbomCommand()
}

class JakeCmd:
# Whether debug output is enabled
_DEBUG_ENABLED: bool = False

# Argument Parser
_arg_parser: argparse.ArgumentParser

# Parsed Arguments
_arguments: argparse.Namespace

# Sub Commands
_subcommands: Dict[str, BaseCommand] = []

# Rich Console
_console: Console
class JakeCmd:

def __init__(self):
def __init__(self, args: argparse.Namespace) -> None:
self._arguments = args
self._console = Console()

# Build and parse command arguments
self._load_subcommands()
self._build_arg_parser()
self._parse_arguments()

if self._arguments.debug_enabled:
self._DEBUG_ENABLED = True
self._debug_message('!!! DEBUG MODE ENABLED !!!')
self._debug_message('Parsed Arguments: {}'.format(self._arguments))

def execute(self):
@staticmethod
def get_arg_parser() -> ArgumentParser:
arg_parser = ArgumentParser(description='Put your Python dependencies in a chokehold')

# Add global options
arg_parser.add_argument('-v', '--version', help='show which version of jake you are running',
action='version',
version=f'jake {jake_version}')
arg_parser.add_argument('-w', '--warn-only', action='store_true', dest='warn_only',
help='prevents exit with non-zero code when issues have been detected')
arg_parser.add_argument('-X', action='store_true', help='enable debug output', dest='debug_enabled')

subparsers = arg_parser.add_subparsers(title='Jake sub-commands', dest='cmd', metavar='')
for subcommand in _SUB_COMMANDS.keys():
_SUB_COMMANDS[subcommand].setup_argument_parser(
arg_parser=subparsers.add_parser(
name=_SUB_COMMANDS[subcommand].get_argument_parser_name(),
help=_SUB_COMMANDS[subcommand].get_argument_parser_help()
)
)

return arg_parser

def execute(self) -> None:
# Show the Jake header
self._print_jake_header()

# Determine primary command and then hand off to that Command handler
if self._arguments.cmd:
command = self._subcommands[self._arguments.cmd]
if self._arguments.cmd and self._arguments.cmd in _SUB_COMMANDS.keys():
command = _SUB_COMMANDS[self._arguments.cmd]
exit_code: int = command.execute(arguments=self._arguments)
exit(exit_code)
else:
self._arg_parser.print_help()

def _load_subcommands(self):
self._subcommands = {
# 'config': ConfigCommand(),
'iq': IqCommand(),
'ddt': OssCommand(),
'sbom': SbomCommand()
}

def _build_arg_parser(self):
self._arg_parser = argparse.ArgumentParser(description='Put your Python dependencies in a chokehold')
JakeCmd.get_arg_parser().print_help()

# Add global options
self._arg_parser.add_argument('-v', '--version', help='show which version of jake you are running',
action='version',
version=f'jake {_jake_version}')
self._arg_parser.add_argument('-w', '--warn-only', action='store_true', dest='warn_only',
help='prevents exit with non-zero code when issues have been detected')
self._arg_parser.add_argument('-X', action='store_true', help='enable debug output', dest='debug_enabled')

subparsers = self._arg_parser.add_subparsers(title='Jake sub-commands', dest='cmd', metavar='')
for subcommand in self._subcommands.keys():
self._subcommands[subcommand].setup_argument_parser(subparsers=subparsers)

def _debug_message(self, message: str):
def _debug_message(self, message: str) -> None:
if self._DEBUG_ENABLED:
print('[DEBUG] - {} - {}'.format(datetime.now(), message))

def _print_jake_header(self):
def _print_jake_header(self) -> None:
""" Prints the banner, most of the user facing commands start with this """
self._console.print(figlet_format('Jake', font='isometric4'), style='dark_green')
self._console.print(figlet_format('..the snake..', font='invita'), style='dark_green')
print("Jake Version: {}".format(_jake_version))
print("Jake Version: {}".format(jake_version))
print('Put your Python dependencies in a chokehold')
print('')

@staticmethod
def _error_and_exit(message: str, exit_code: int = 1):
def _error_and_exit(message: str, exit_code: int = 1) -> None:
print('[ERROR] - {} - {}'.format(datetime.now(), message))
exit(exit_code)

def _parse_arguments(self):
self._arguments = self._arg_parser.parse_args()


# only for testing
def _create_cmd():
return JakeCmd()


def main():
JakeCmd().execute()
def main() -> None:
parser = JakeCmd.get_arg_parser()
args = parser.parse_args()
JakeCmd(args).execute()


if __name__ == "__main__":
Expand Down
37 changes: 27 additions & 10 deletions jake/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,52 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import sys
from abc import ABC, abstractmethod
from argparse import ArgumentParser, Namespace
from typing import Optional

if sys.version_info >= (3, 8):
from importlib.metadata import version as meta_version
else:
from importlib_metadata import version as meta_version

jake_version: str = 'TBC'
try:
_jake_version: Optional[str] = str(meta_version('jake')) # type: ignore[no-untyped-call]
jake_version = str(meta_version('jake')) # type: ignore[no-untyped-call]
except Exception:
_jake_version = 'DEVELOPMENT'
jake_version = 'DEVELOPMENT'


class BaseCommand(ABC):
# Parsed Arguments
_arguments: argparse.Namespace

def __init__(self) -> None:
super().__init__()
self._arguments: Optional[Namespace] = None

def execute(self, arguments: Namespace) -> int:
self._arguments = arguments
return self.handle_args()

@abstractmethod
def handle_args(self) -> int:
pass

def execute(self, arguments: argparse.Namespace) -> int:
self._arguments = arguments
return self.handle_args()
@abstractmethod
def get_argument_parser_name(self) -> str:
pass

@abstractmethod
def setup_argument_parser(self, subparsers: argparse._SubParsersAction):
def get_argument_parser_help(self) -> str:
pass

@abstractmethod
def setup_argument_parser(self, arg_parser: ArgumentParser) -> None:
pass

@property
def arguments(self) -> Namespace:
if self._arguments:
return self._arguments

raise ValueError('Arguments have not been set yet - execute() has to be called first')
20 changes: 12 additions & 8 deletions jake/command/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from argparse import ArgumentParser

import argparse
from . import BaseCommand


class ConfigCommand(BaseCommand):

def setup_argument_parser(self, subparsers: argparse._SubParsersAction):
parser_config: argparse.ArgumentParser = subparsers.add_parser(
'config',
help='configure jake for OSS Index or Nexus Lifecycle access'
)
def handle_args(self) -> int:
pass

parser_config.add_argument('oss', help='configure Nexus IQ Server or OSSIndex', nargs='?',
choices=('iq', 'oss'))
def get_argument_parser_name(self) -> str:
return 'config'

def get_argument_parser_help(self) -> str:
return 'configure jake for OSS Index or Nexus Lifecycle access'

def setup_argument_parser(self, arg_parser: ArgumentParser) -> None:
arg_parser.add_argument('oss', help='configure Nexus IQ Server or OSSIndex', nargs='?',
choices=('iq', 'oss'))
Loading

0 comments on commit a0ab7ee

Please sign in to comment.