Skip to content

Commit

Permalink
Merge branch 'feat/validate_cli_arguments' into 'main'
Browse files Browse the repository at this point in the history
Feat/validate cli arguments

Closes PACMAN-924

See merge request espressif/idf-component-manager!459
  • Loading branch information
kumekay committed Dec 5, 2024
2 parents fdf1111 + 32a17dd commit 03660f0
Show file tree
Hide file tree
Showing 19 changed files with 687 additions and 183 deletions.
21 changes: 19 additions & 2 deletions idf_component_manager/cli/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@

import click

from idf_component_manager.cli.validations import (
validate_git_url,
validate_if_archive,
validate_sha,
validate_version,
)

from .constants import (
get_dest_dir_option,
get_name_option,
Expand Down Expand Up @@ -41,11 +48,13 @@ def component():
'--repository',
default=None,
help='The URL of the component repository. This option overwrites the value in the idf_component.yml',
callback=validate_git_url,
),
click.option(
'--commit-sha',
default=None,
help='Git commit SHA of the the component version. This option overwrites the value in the idf_component.yml',
callback=validate_sha,
),
click.option(
'--repository-path',
Expand Down Expand Up @@ -95,6 +104,7 @@ def pack(
'--archive',
help='Path of the archive with a component to upload. '
'When not provided the component will be packed automatically.',
callback=validate_if_archive,
)
@click.option(
'--skip-pre-release',
Expand Down Expand Up @@ -168,7 +178,9 @@ def upload_status(manager, profile_name, job):

@component.command()
@add_options(PROJECT_OPTIONS + NAMESPACE_NAME_OPTIONS)
@click.option('--version', required=True, help='Component version to delete.')
@click.option(
'--version', required=True, help='Component version to delete.', callback=validate_version
)
def delete(manager, profile_name, namespace, name, version):
"""
Delete specified version of the component from the component registry.
Expand All @@ -178,7 +190,12 @@ def delete(manager, profile_name, namespace, name, version):

@component.command()
@add_options(PROJECT_OPTIONS + NAMESPACE_NAME_OPTIONS)
@click.option('--version', required=True, help='Component version to yank version.')
@click.option(
'--version',
required=True,
help='Component version to yank version.',
callback=validate_version,
)
@click.option(
'--message',
required=True,
Expand Down
12 changes: 10 additions & 2 deletions idf_component_manager/cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
import click
from click.decorators import FC

from idf_component_manager.cli.validations import (
combined_callback,
validate_existing_dir,
validate_name,
)
from idf_component_manager.core import ComponentManager
from idf_component_manager.utils import validate_name


def get_project_dir_option() -> t.List[FC]:
Expand All @@ -16,7 +20,10 @@ def get_project_dir_option() -> t.List[FC]:
'--project-dir',
'manager',
default=os.getcwd(),
callback=lambda ctx, param, value: ComponentManager(value), # noqa: ARG005
callback=combined_callback(
validate_existing_dir,
lambda ctx, param, value: ComponentManager(value), # noqa: ARG005
),
),
]

Expand Down Expand Up @@ -47,6 +54,7 @@ def get_namespace_option() -> t.List[FC]:
'--namespace',
envvar='IDF_COMPONENT_NAMESPACE',
default=None,
callback=validate_name,
help='Namespace for the component. Can be set in config file.',
),
]
Expand Down
19 changes: 16 additions & 3 deletions idf_component_manager/cli/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

import click

from idf_component_manager.cli.validations import (
validate_add_dependency,
validate_existing_dir,
validate_git_url,
validate_url,
)
from idf_component_tools.manifest import MANIFEST_JSON_SCHEMA

from .constants import get_profile_option, get_project_dir_option
Expand Down Expand Up @@ -39,11 +45,14 @@ def schema():
'--path',
default=None,
help='Path to the component where the dependency will be added. The component name is ignored when the path is specified.',
callback=validate_existing_dir,
),
]

GIT_OPTIONS = [
click.option('--git', default=None, help='Git URL of the component.'),
click.option(
'--git', default=None, help='Git URL of the component.', callback=validate_git_url
),
click.option(
'--git-path', default='.', help='Path to the component in the git repository.'
),
Expand Down Expand Up @@ -78,9 +87,13 @@ def create(manager, component, path):
+ PROFILE_OPTION
+ MANIFEST_OPTIONS
+ GIT_OPTIONS
+ [click.option('--registry-url', default=None, help='URL of the registry.')]
+ [
click.option(
'--registry-url', default=None, help='URL of the registry.', callback=validate_url
)
]
)
@click.argument('dependency', required=True)
@click.argument('dependency', required=True, callback=validate_add_dependency)
def add_dependency(
manager, profile_name, component, path, dependency, registry_url, git, git_path, git_ref
):
Expand Down
3 changes: 3 additions & 0 deletions idf_component_manager/cli/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# SPDX-License-Identifier: Apache-2.0
import click

from idf_component_manager.cli.validations import validate_path_for_project

from .constants import get_project_dir_option, get_project_options
from .utils import add_options

Expand All @@ -25,6 +27,7 @@ def project():
default=None,
help='Path of the new project. '
'The project will be created directly in the given folder if it is empty.',
callback=validate_path_for_project,
)
@click.argument('example', required=True)
def create_from_example(manager, example, path, profile_name):
Expand Down
17 changes: 13 additions & 4 deletions idf_component_manager/cli/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import click
import requests

from idf_component_manager.cli.validations import (
combined_callback,
validate_name,
validate_registry_component,
validate_url,
)
from idf_component_manager.core import ComponentManager
from idf_component_manager.utils import VersionSolverResolution
from idf_component_tools import warn
Expand Down Expand Up @@ -43,23 +49,25 @@ def registry():
@click.option(
'--default-namespace',
help='Default namespace to use for the components',
callback=validate_name,
)
@click.option(
'--default_namespace',
help="This argument has been deprecated by 'default-namespace'",
hidden=True,
callback=deprecated_option,
callback=combined_callback(deprecated_option, validate_name),
expose_value=False,
)
@click.option(
'--registry-url',
help='URL of the registry to use',
callback=validate_url,
)
@click.option(
'--registry_url',
help="This argument has been deprecated by '--registry-url'",
hidden=True,
callback=deprecated_option,
callback=combined_callback(deprecated_option, validate_url),
expose_value=False,
)
def login(profile_name, no_browser, description, default_namespace, registry_url):
Expand All @@ -77,8 +85,8 @@ def login(profile_name, no_browser, description, default_namespace, registry_url
# Check if token is already in the profile
if profile.api_token:
raise FatalError(
'You are already logged in with profile "{}", '
'please either logout or use different profile'.format(profile_name)
f'You are already logged in with profile "{profile_name}", '
'please either logout or use a different profile'
)

api_client = get_api_client(
Expand Down Expand Up @@ -186,6 +194,7 @@ def logout(profile_name, no_revoke):
help='Specify the components to sync from the registry. '
'Use multiple --component options for multiple components. '
'Format: namespace/name<version_spec>. Example: example/cmp==1.0.0',
callback=validate_registry_component,
)
@click.option(
'--resolution',
Expand Down
158 changes: 158 additions & 0 deletions idf_component_manager/cli/validations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import re
from pathlib import Path
from urllib.parse import urlparse

import click

from idf_component_manager.core_utils import COMPONENT_FULL_NAME_WITH_SPEC_REGEX
from idf_component_tools.archive_tools import ArchiveError, get_format_from_path
from idf_component_tools.constants import COMPILED_COMMIT_ID_RE, COMPILED_GIT_URL_RE
from idf_component_tools.manifest import WEB_DEPENDENCY_REGEX
from idf_component_tools.manifest.constants import SLUG_REGEX
from idf_component_tools.semver import Version
from idf_component_tools.semver.base import SimpleSpec


def validate_name(ctx, param, value): # noqa: ARG001
if value is not None:
name = value.lower()

if not re.match(SLUG_REGEX, name):
raise click.BadParameter(
f'"{name}" should consist of 2 or more letters, numbers, "-" or "_". '
'It cannot start or end with "_" or "-", or have sequences of these characters.'
)
return name


def validate_existing_dir(ctx, param, value): # noqa: ARG001
if value is not None:
if not value or not Path(value).is_dir():
raise click.BadParameter(f'"{value}" directory does not exist.')
return value


def validate_url(ctx, param, value): # noqa: ARG001
if value is not None:
result = urlparse(value)
if not result.scheme or not result.hostname:
raise click.BadParameter('Invalid URL.')
return value


def validate_sha(ctx, param, value): # noqa: ARG001
if value is not None and not COMPILED_COMMIT_ID_RE.match(value):
raise click.BadParameter('Invalid SHA-1 hash.')
return value


def validate_git_url(ctx, param, value): # noqa: ARG001
if value is not None and not COMPILED_GIT_URL_RE.match(value):
raise click.BadParameter('Invalid Git remote URL.')
return value


def validate_path_for_project(ctx, param, value): # noqa: ARG001
if value is not None:
project_path = Path(value)
if project_path.is_file():
raise click.BadParameter(
f'Your target path is not a directory. '
f'Please remove the {project_path.resolve()} or use a different target path.'
)

if project_path.is_dir() and any(project_path.iterdir()):
raise click.BadParameter(
f'The directory "{project_path}" is not empty. '
'To create an example you must empty the directory or '
'choose a different path.',
)
return value


def validate_if_archive(ctx, param, value): # noqa: ARG001
if value is not None:
if not Path(value).is_file():
raise click.BadParameter(
f'Cannot find archive to upload: {value}. Please check the path or if it exists.'
)
try:
get_format_from_path(value)
except ArchiveError:
raise click.BadParameter(f'Unknown archive extension for file: {value}')
return value


def validate_version(ctx, param, value): # noqa: ARG001
if value is not None:
try:
Version.parse(value)
except ValueError:
raise click.BadParameter(
f'Invalid version scheme.\n'
f'Received: "{value}"\n'
'Documentation: https://docs.espressif.com/projects/idf-component-manager/en/'
'latest/reference/versioning.html#versioning-scheme'
)
return value


def validate_registry_component(ctx, param, value): # noqa: ARG001
if value is not None:
for component in value:
match = re.match(COMPONENT_FULL_NAME_WITH_SPEC_REGEX, component)
if not match:
raise click.BadParameter(
'Cannot parse COMPONENT argument. '
'Please use format like: namespace/component=1.0.0'
)

version_spec = match.group('version') or '*'

try:
SimpleSpec(version_spec)
except ValueError:
raise click.BadParameter(
f'Invalid version specification: "{version_spec}". Please use format like ">=1" or "*".'
)
return value


def validate_add_dependency(ctx, param, value): # noqa: ARG001
if not value:
raise click.BadParameter('Name of the dependency can not be an empty string')

if 'git' not in ctx.params:
match = re.match(WEB_DEPENDENCY_REGEX, value)
if match:
_, spec = match.groups()
else:
raise click.BadParameter(
f'Invalid dependency: "{value}". Please use format "namespace/name".'
)

if not spec:
spec = '*'

try:
SimpleSpec(spec)
except ValueError:
raise click.BadParameter(
f'Invalid dependency version requirement: {spec}. '
'Please use format like ">=1" or "*".'
)

return value


# Function to combine multiple callback, order sensetive - each callback will be executed in the order in which it was passed
# If passed callback terminate command execution, it will terminate execution of loop as well
def combined_callback(*callbacks):
def wrapper(ctx, param, value):
for callback in callbacks:
value = callback(ctx, param, value)
return value

return wrapper
Loading

0 comments on commit 03660f0

Please sign in to comment.