diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..6c49b631a --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,17 @@ +name: Black formatting (79 chars lines) + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + black: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable + with: + options: "--check --diff --line-length 79 --extend-exclude '/vendor/'" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17ef8b267..5db79c7bd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,9 @@ its providers and its documentation. ## General recommendations +- The codebase uses [black] for formatting, with `line-length` set to `79`. +- Use `git config blame.ignoreRevsFile .git-blame-ignore-revs` if you want to +[ignore commits related to black formatting]. - Setup your editor of choice to run [autopep8] on save. This helps keep everything passing [flake8]. - The code doesn’t have to be pylint-clean, but @@ -385,6 +388,8 @@ review of the source files. Once all is good, you can submit your documentation change like any other changes using a pull request. +[black]: https://black.readthedocs.io/ +[ignore commits related to black formatting]: https://black.readthedocs.io/en/stable/guides/introducing_black_to_your_project.html#avoiding-ruining-git-blame [autopep8]: https://pypi.org/project/autopep8/ [flake8]: https://flake8.pycqa.org/en/latest/ [pylint]: https://www.pylint.org/ diff --git a/checkbox-ng/checkbox_ng/__init__.py b/checkbox-ng/checkbox_ng/__init__.py index 3082e0c51..30c8bb8af 100644 --- a/checkbox-ng/checkbox_ng/__init__.py +++ b/checkbox-ng/checkbox_ng/__init__.py @@ -33,5 +33,6 @@ __version__ = version(__package__) except PackageNotFoundError: import logging - logging.error('Failed to retrieve checkbox-ng version') - __version__ = 'unknown' + + logging.error("Failed to retrieve checkbox-ng version") + __version__ = "unknown" diff --git a/checkbox-ng/checkbox_ng/certification.py b/checkbox-ng/checkbox_ng/certification.py index 4c75c3f7b..347f039f2 100644 --- a/checkbox-ng/checkbox_ng/certification.py +++ b/checkbox-ng/checkbox_ng/certification.py @@ -47,7 +47,7 @@ class SubmissionServiceTransport(TransportBase): - Payload can be in: * LZMA compressed tarball that includes a submission.json and results from checkbox. - """ + """ def __init__(self, where, options): """ @@ -57,7 +57,7 @@ def __init__(self, where, options): a 15-character (or longer) alphanumeric ID for the system. """ super().__init__(where, options) - self._secure_id = self.options.get('secure_id') + self._secure_id = self.options.get("secure_id") if self._secure_id is not None: self._validate_secure_id(self._secure_id) @@ -91,19 +91,19 @@ def send(self, data, config=None, session_state=None): if secure_id is None: raise InvalidSecureIDError(_("Secure ID not specified")) self._validate_secure_id(secure_id) - logger.debug( - _("Sending to %s, Secure ID is %s"), self.url, secure_id) + logger.debug(_("Sending to %s, Secure ID is %s"), self.url, secure_id) try: response = requests.post(self.url, data=data) except requests.exceptions.Timeout as exc: raise TransportError( - _("Request to {0} timed out: {1}").format(self.url, exc)) + _("Request to {0} timed out: {1}").format(self.url, exc) + ) except requests.exceptions.InvalidSchema as exc: - raise TransportError( - _("Invalid destination URL: {0}").format(exc)) + raise TransportError(_("Invalid destination URL: {0}").format(exc)) except requests.exceptions.ConnectionError as exc: raise TransportError( - _("Unable to connect to {0}: {1}").format(self.url, exc)) + _("Unable to connect to {0}: {1}").format(self.url, exc) + ) if response is not None: try: # This will raise HTTPError for status != 20x @@ -121,8 +121,10 @@ def send(self, data, config=None, session_state=None): def _validate_secure_id(self, secure_id): if not re.match(SECURE_ID_PATTERN, secure_id): - message = _(( - "{} is not a valid secure_id. secure_id must be a " - "15-character (or more) alphanumeric string" - ).format(secure_id)) + message = _( + ( + "{} is not a valid secure_id. secure_id must be a " + "15-character (or more) alphanumeric string" + ).format(secure_id) + ) raise InvalidSecureIDError(message) diff --git a/checkbox-ng/checkbox_ng/config.py b/checkbox-ng/checkbox_ng/config.py index bc7c263a8..8a3566531 100644 --- a/checkbox-ng/checkbox_ng/config.py +++ b/checkbox-ng/checkbox_ng/config.py @@ -84,7 +84,9 @@ def load_configs(launcher_file=None, cfg=None): - anything that /etc/xdg/A imports has the lowest possible priority """ - assert not (launcher_file and cfg), "config_filename in cfg will be ignored, FIXME" + assert not ( + launcher_file and cfg + ), "config_filename in cfg will be ignored, FIXME" if not cfg: cfg = Configuration() if launcher_file: diff --git a/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py b/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py index ce6239fb0..a3a93dafe 100644 --- a/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py +++ b/checkbox-ng/checkbox_ng/launcher/checkbox_cli.py @@ -57,6 +57,7 @@ class Context: def __init__(self, args, sa): self.args = args self.sa = sa + def reset_sa(self): self.sa = SessionAssistant() diff --git a/checkbox-ng/checkbox_ng/launcher/controller.py b/checkbox-ng/checkbox_ng/launcher/controller.py index bcb4eadf4..481316715 100644 --- a/checkbox-ng/checkbox_ng/launcher/controller.py +++ b/checkbox-ng/checkbox_ng/launcher/controller.py @@ -365,7 +365,9 @@ def should_start_via_launcher(self): tp_forced = self.launcher.get_value("test plan", "forced") chosen_tp = self.launcher.get_value("test plan", "unit") if tp_forced and not chosen_tp: - raise SystemExit("The test plan selection was forced but no unit was provided") # split me into lines + raise SystemExit( + "The test plan selection was forced but no unit was provided" + ) # split me into lines return tp_forced @contextlib.contextmanager diff --git a/checkbox-ng/checkbox_ng/launcher/merge_reports.py b/checkbox-ng/checkbox_ng/launcher/merge_reports.py index d251b2fc0..43c0a4d5d 100644 --- a/checkbox-ng/checkbox_ng/launcher/merge_reports.py +++ b/checkbox-ng/checkbox_ng/launcher/merge_reports.py @@ -36,88 +36,99 @@ #: Name-space prefix for Canonical Certification -CERTIFICATION_NS = 'com.canonical.certification::' +CERTIFICATION_NS = "com.canonical.certification::" -class MergeReports(): +class MergeReports: def register_arguments(self, parser): parser.add_argument( - 'submission', nargs='*', metavar='SUBMISSION', - help='submission tarball') + "submission", + nargs="*", + metavar="SUBMISSION", + help="submission tarball", + ) parser.add_argument( - '-o', '--output-file', metavar='FILE', required=True, - help='save combined test results to the specified FILE') + "-o", + "--output-file", + metavar="FILE", + required=True, + help="save combined test results to the specified FILE", + ) def _parse_submission(self, submission, tmpdir, mode="list"): try: with tarfile.open(submission) as tar: tar.extractall(tmpdir.name) - with open(os.path.join( - tmpdir.name, 'submission.json')) as f: + with open(os.path.join(tmpdir.name, "submission.json")) as f: data = json.load(f) - for result in data['results']: - result['plugin'] = 'shell' # Required so default to shell - result['summary'] = result['name'] + for result in data["results"]: + result["plugin"] = "shell" # Required so default to shell + result["summary"] = result["name"] # 'id' field in json file only contains partial id - result['id'] = result.get('full_id', result['id']) - if "::" not in result['id']: - result['id'] = CERTIFICATION_NS + result['id'] + result["id"] = result.get("full_id", result["id"]) + if "::" not in result["id"]: + result["id"] = CERTIFICATION_NS + result["id"] if mode == "list": self.job_list.append(JobDefinition(result)) elif mode == "dict": - self.job_dict[result['id']] = JobDefinition(result) - for result in data['resource-results']: - result['plugin'] = 'resource' - result['summary'] = result['name'] + self.job_dict[result["id"]] = JobDefinition(result) + for result in data["resource-results"]: + result["plugin"] = "resource" + result["summary"] = result["name"] # 'id' field in json file only contains partial id - result['id'] = result.get('full_id', result['id']) - if "::" not in result['id']: - result['id'] = CERTIFICATION_NS + result['id'] + result["id"] = result.get("full_id", result["id"]) + if "::" not in result["id"]: + result["id"] = CERTIFICATION_NS + result["id"] if mode == "list": self.job_list.append(JobDefinition(result)) elif mode == "dict": - self.job_dict[result['id']] = JobDefinition(result) - for result in data['attachment-results']: - result['plugin'] = 'attachment' - result['summary'] = result['name'] + self.job_dict[result["id"]] = JobDefinition(result) + for result in data["attachment-results"]: + result["plugin"] = "attachment" + result["summary"] = result["name"] # 'id' field in json file only contains partial id - result['id'] = result.get('full_id', result['id']) - if "::" not in result['id']: - result['id'] = CERTIFICATION_NS + result['id'] + result["id"] = result.get("full_id", result["id"]) + if "::" not in result["id"]: + result["id"] = CERTIFICATION_NS + result["id"] if mode == "list": self.job_list.append(JobDefinition(result)) elif mode == "dict": - self.job_dict[result['id']] = JobDefinition(result) - for cat_id, cat_name in data['category_map'].items(): + self.job_dict[result["id"]] = JobDefinition(result) + for cat_id, cat_name in data["category_map"].items(): if mode == "list": self.category_list.append( - CategoryUnit({'id': cat_id, 'name': cat_name})) + CategoryUnit({"id": cat_id, "name": cat_name}) + ) elif mode == "dict": self.category_dict[cat_id] = CategoryUnit( - {'id': cat_id, 'name': cat_name}) + {"id": cat_id, "name": cat_name} + ) except OSError as e: raise SystemExit(e) except KeyError as e: self._output_potential_action(str(e)) raise SystemExit(e) - return data['title'] + return data["title"] def _populate_session_state(self, job, state): io_log = [ - IOLogRecord(count, 'stdout', line.encode('utf-8')) + IOLogRecord(count, "stdout", line.encode("utf-8")) for count, line in enumerate( - job.get_record_value('io_log').splitlines( - keepends=True)) + job.get_record_value("io_log").splitlines(keepends=True) + ) ] - result = MemoryJobResult({ - 'outcome': job.get_record_value('outcome', - job.get_record_value('status')), - 'comments': job.get_record_value('comments'), - 'execution_duration': job.get_record_value('duration'), - 'io_log': io_log, - }) + result = MemoryJobResult( + { + "outcome": job.get_record_value( + "outcome", job.get_record_value("status") + ), + "comments": job.get_record_value("comments"), + "execution_duration": job.get_record_value("duration"), + "io_log": io_log, + } + ) state.update_job_result(job, result) - if job.plugin == 'resource': + if job.plugin == "resource": new_resource_list = [] for record in gen_rfc822_records_from_io_log(job, result): resource = Resource(record.data) @@ -127,29 +138,34 @@ def _populate_session_state(self, job, state): state.set_resource_list(job.id, new_resource_list) job_state = state.job_state_map[job.id] job_state.effective_category_id = job.get_record_value( - 'category_id', 'com.canonical.plainbox::uncategorised') + "category_id", "com.canonical.plainbox::uncategorised" + ) job_state.effective_certification_status = job.get_record_value( - 'certification_status', 'unspecified') + "certification_status", "unspecified" + ) def _create_exporter(self, exporter_id): exporter_map = {} exporter_units = get_exporters().unit_list for unit in exporter_units: - if unit.Meta.name == 'exporter': + if unit.Meta.name == "exporter": support = unit.support if support: exporter_map[unit.id] = support exporter_support = exporter_map[exporter_id] return exporter_support.exporter_cls( - [], exporter_unit=exporter_support) + [], exporter_unit=exporter_support + ) def _output_potential_action(self, message): hint = "" - keys = ['resource', 'attachment'] + keys = ["resource", "attachment"] for key in keys: if key in message: - hint = ("Make sure your input submission provides {}-related " - "information.".format(key)) + hint = ( + "Make sure your input submission provides {}-related " + "information.".format(key) + ) if hint: print("Fail to merge. " + hint) else: @@ -163,13 +179,15 @@ def invoked(self, ctx): self.category_list = [] session_title = self._parse_submission(submission, tmpdir) manager = SessionManager.create_with_unit_list( - self.job_list + self.category_list) + self.job_list + self.category_list + ) manager.state.metadata.title = session_title for job in self.job_list: self._populate_session_state(job, manager.state) manager_list.append(manager) exporter = self._create_exporter( - 'com.canonical.plainbox::html-multi-page') - with open(ctx.args.output_file, 'wb') as stream: + "com.canonical.plainbox::html-multi-page" + ) + with open(ctx.args.output_file, "wb") as stream: exporter.dump_from_session_manager_list(manager_list, stream) print(ctx.args.output_file) diff --git a/checkbox-ng/checkbox_ng/launcher/merge_submissions.py b/checkbox-ng/checkbox_ng/launcher/merge_submissions.py index adab55026..d0416b27b 100644 --- a/checkbox-ng/checkbox_ng/launcher/merge_submissions.py +++ b/checkbox-ng/checkbox_ng/launcher/merge_submissions.py @@ -28,18 +28,28 @@ class MergeSubmissions(MergeReports): - name = 'merge-submissions' + name = "merge-submissions" def register_arguments(self, parser): parser.add_argument( - 'submission', nargs='*', metavar='SUBMISSION', - help='submission tarball') + "submission", + nargs="*", + metavar="SUBMISSION", + help="submission tarball", + ) parser.add_argument( - '-o', '--output-file', metavar='FILE', required=True, - help='save combined test results to the specified FILE') + "-o", + "--output-file", + metavar="FILE", + required=True, + help="save combined test results to the specified FILE", + ) parser.add_argument( - '--title', action='store', metavar='SESSION_NAME', - help='title of the session to use') + "--title", + action="store", + metavar="SESSION_NAME", + help="title of the session to use", + ) def invoked(self, ctx): tmpdir = TemporaryDirectory() @@ -47,18 +57,19 @@ def invoked(self, ctx): self.category_dict = {} for submission in ctx.args.submission: session_title = self._parse_submission( - submission, tmpdir, mode='dict') + submission, tmpdir, mode="dict" + ) manager = SessionManager.create_with_unit_list( - list(self.job_dict.values()) + list(self.category_dict.values())) + list(self.job_dict.values()) + list(self.category_dict.values()) + ) manager.state.metadata.title = ctx.args.title or session_title for job in self.job_dict.values(): self._populate_session_state(job, manager.state) - exporter = self._create_exporter( - 'com.canonical.plainbox::tar') - with open(ctx.args.output_file, 'wb') as stream: + exporter = self._create_exporter("com.canonical.plainbox::tar") + with open(ctx.args.output_file, "wb") as stream: exporter.dump_from_session_manager(manager, stream) with tarfile.open(ctx.args.output_file) as tar: tar.extractall(tmpdir.name) - with tarfile.open(ctx.args.output_file, mode='w:xz') as tar: - tar.add(tmpdir.name, arcname='') + with tarfile.open(ctx.args.output_file, mode="w:xz") as tar: + tar.add(tmpdir.name, arcname="") print(ctx.args.output_file) diff --git a/checkbox-ng/checkbox_ng/launcher/provider_tools.py b/checkbox-ng/checkbox_ng/launcher/provider_tools.py index 065f23ae1..ee74832db 100644 --- a/checkbox-ng/checkbox_ng/launcher/provider_tools.py +++ b/checkbox-ng/checkbox_ng/launcher/provider_tools.py @@ -21,10 +21,12 @@ def main(): - manage_f = os.path.join(os.getcwd(), 'manage.py') + manage_f = os.path.join(os.getcwd(), "manage.py") if not os.path.exists(manage_f): - raise SystemExit('Could not find manage.py in current directory.' - ' Is this a plainbox provider?') - spec = importlib.util.spec_from_file_location('setup', manage_f) + raise SystemExit( + "Could not find manage.py in current directory." + " Is this a plainbox provider?" + ) + spec = importlib.util.spec_from_file_location("setup", manage_f) foo = importlib.util.module_from_spec(spec) spec.loader.exec_module(foo) diff --git a/checkbox-ng/checkbox_ng/launcher/run.py b/checkbox-ng/checkbox_ng/launcher/run.py index 3f927c18b..17e1d0b9f 100644 --- a/checkbox-ng/checkbox_ng/launcher/run.py +++ b/checkbox-ng/checkbox_ng/launcher/run.py @@ -57,10 +57,12 @@ def __init__(self, action_list, prompt=None, color=None): def run(self): long_hint = "\n".join( " {accel} => {label}".format( - accel=self.C.BLUE(action.accel) if action.accel else ' ', - label=action.label) - for action in self.action_list) - short_hint = ''.join(action.accel for action in self.action_list) + accel=self.C.BLUE(action.accel) if action.accel else " ", + label=action.label, + ) + for action in self.action_list + ) + short_hint = "".join(action.accel for action in self.action_list) while True: try: print(self.C.BLUE(self.prompt)) @@ -127,10 +129,7 @@ def noreturn_job(self): class NormalUI(IJobRunnerUI): - STREAM_MAP = { - 'stdout': sys.stdout, - 'stderr': sys.stderr - } + STREAM_MAP = {"stdout": sys.stdout, "stderr": sys.stderr} def __init__(self, color, show_cmd_output=True): self.show_cmd_output = show_cmd_output @@ -138,7 +137,7 @@ def __init__(self, color, show_cmd_output=True): self._color = color def considering_job(self, job, job_state): - print(self.C.header(job.tr_summary(), fill='-')) + print(self.C.header(job.tr_summary(), fill="-")) print(_("ID: {0}").format(job.id)) print(_("Category: {0}").format(job_state.effective_category_id)) @@ -146,19 +145,21 @@ def about_to_start_running(self, job, job_state): pass def wait_for_interaction_prompt(self, job): - return self.pick_action_cmd([ - Action('', _("press ENTER to continue"), 'run'), - Action('c', _('add a comment'), 'comment'), - Action('s', _("skip this job"), 'skip'), - Action('q', _("save the session and quit"), 'quit') - ]) + return self.pick_action_cmd( + [ + Action("", _("press ENTER to continue"), "run"), + Action("c", _("add a comment"), "comment"), + Action("s", _("skip this job"), "skip"), + Action("q", _("save the session and quit"), "quit"), + ] + ) def started_running(self, job, job_state): pass def about_to_execute_program(self, args, kwargs): if self.show_cmd_output: - print(self.C.BLACK("... 8< -".ljust(80, '-'))) + print(self.C.BLACK("... 8< -".ljust(80, "-"))) else: print(self.C.BLACK("(" + _("Command output hidden") + ")")) @@ -166,17 +167,12 @@ def got_program_output(self, stream_name, line): if not self.show_cmd_output: return stream = self.STREAM_MAP[stream_name] - stream = { - 'stdout': sys.stdout, - 'stderr': sys.stderr - }[stream_name] + stream = {"stdout": sys.stdout, "stderr": sys.stderr}[stream_name] try: - if stream_name == 'stdout': - print(self.C.GREEN(line.decode("UTF-8")), - end='', file=stream) - elif stream_name == 'stderr': - print(self.C.RED(line.decode("UTF-8")), - end='', file=stream) + if stream_name == "stdout": + print(self.C.GREEN(line.decode("UTF-8")), end="", file=stream) + elif stream_name == "stderr": + print(self.C.RED(line.decode("UTF-8")), end="", file=stream) except UnicodeDecodeError: self.show_cmd_output = False print(self.C.BLACK("(" + _("Hiding binary test output") + ")")) @@ -184,7 +180,7 @@ def got_program_output(self, stream_name, line): def finished_executing_program(self, returncode): if self.show_cmd_output: - print(self.C.BLACK("- >8 ---".rjust(80, '-'))) + print(self.C.BLACK("- >8 ---".rjust(80, "-"))) def finished_running(self, job, state, result): pass @@ -231,8 +227,11 @@ def pick_action_cmd(self, action_list, prompt=None): return ActionUI(action_list, prompt, self._color).run() def noreturn_job(self): - print(self.C.RED(_("Waiting for the system to shut down or" - " reboot..."))) + print( + self.C.RED( + _("Waiting for the system to shut down or" " reboot...") + ) + ) class ReRunJob(Exception): @@ -243,6 +242,6 @@ class ReRunJob(Exception): def seconds_to_human_duration(seconds: float) -> str: - """ Convert ammount of seconds to human readable duration string. """ + """Convert ammount of seconds to human readable duration string.""" delta = datetime.timedelta(seconds=round(seconds)) return str(delta) diff --git a/checkbox-ng/checkbox_ng/launcher/stages.py b/checkbox-ng/checkbox_ng/launcher/stages.py index 798e71c97..5e2b94d5e 100644 --- a/checkbox-ng/checkbox_ng/launcher/stages.py +++ b/checkbox-ng/checkbox_ng/launcher/stages.py @@ -99,7 +99,7 @@ def _run_single_job_with_ui_loop(self, job, ui): print(_("ID: {0}").format(job.id)) print(_("Category: {0}").format(job_state.effective_category_id)) comments = "" - self.sa.note_metadata_starting_job({"id" : job.id}, job_state) + self.sa.note_metadata_starting_job({"id": job.id}, job_state) while True: if job.plugin in ( "user-interact", diff --git a/checkbox-ng/checkbox_ng/launcher/startprovider.py b/checkbox-ng/checkbox_ng/launcher/startprovider.py index d2ad4f716..9b44ed398 100644 --- a/checkbox-ng/checkbox_ng/launcher/startprovider.py +++ b/checkbox-ng/checkbox_ng/launcher/startprovider.py @@ -84,7 +84,8 @@ def __init__(self, name, parent=None, executable=False, full_text=""): def instantiate(self, root, **kwargs): if self.parent: filename = os.path.join( - root, self.parent, self.name.format(**kwargs)) + root, self.parent, self.name.format(**kwargs) + ) else: filename = os.path.join(root, self.name.format(**kwargs)) if os.path.exists(filename): @@ -135,7 +136,8 @@ def instantiate(self, root, **kwargs): super().instantiate(root, **kwargs) for thing in self.things: thing.instantiate( - os.path.join(root, self.name.format(**kwargs)), **kwargs) + os.path.join(root, self.name.format(**kwargs)), **kwargs + ) class EmptyProviderSkeleton(Skeleton): @@ -143,7 +145,11 @@ class EmptyProviderSkeleton(Skeleton): things = [] - things.append(File("manage.py", executable=True, full_text=""" + things.append( + File( + "manage.py", + executable=True, + full_text=""" #!/usr/bin/env python3 from plainbox.provider_manager import setup, N_ @@ -165,7 +171,9 @@ class EmptyProviderSkeleton(Skeleton): description=N_("The {name} provider"), gettext_domain="{gettext_domain}", ) - """)) + """, + ) + ) class ProviderSkeleton(EmptyProviderSkeleton): @@ -189,7 +197,10 @@ class ProviderSkeleton(EmptyProviderSkeleton): po_dir = Directory("po") things.append(po_dir) - things.append(File("README.md", full_text=""" + things.append( + File( + "README.md", + full_text=""" Skeleton for a new PlainBox provider ==================================== @@ -236,11 +247,17 @@ class ProviderSkeleton(EmptyProviderSkeleton): If you find bugs or would like to see additional features developed you can file bugs on the parent project page: https://bugs.launchpad.net/checkbox/+filebug - """)) + """, + ) + ) with units_dir as parent: - things.append(File("examples-trivial.pxu", parent, full_text=""" + things.append( + File( + "examples-trivial.pxu", + parent, + full_text=""" # Two example jobs, both using the 'shell' "plugin". See the # documentation for examples of other test cases including # interactive tests, "resource" tests and a few other types. @@ -288,9 +305,15 @@ class ProviderSkeleton(EmptyProviderSkeleton): estimated_duration: 0.01 command: false flags: preserve-locale - """)) + """, + ) + ) - things.append(File("examples-normal.pxu", parent, full_text=""" + things.append( + File( + "examples-normal.pxu", + parent, + full_text=""" unit: test plan id: normal _name: Examples - normal @@ -351,9 +374,15 @@ class ProviderSkeleton(EmptyProviderSkeleton): estimated_duration: 0.01 command: cat /proc/cpuinfo flags: preserve-locale - """)) + """, + ) + ) - things.append(File("examples-intermediate.pxu", parent, full_text=""" + things.append( + File( + "examples-intermediate.pxu", + parent, + full_text=""" unit: test plan id: intermediate _name: Examples - intermediate @@ -467,21 +496,33 @@ class ProviderSkeleton(EmptyProviderSkeleton): requires: detected_device.type == "WEBCAM" estimated_duration: 30 - """)) + """, + ) + ) with po_dir as parent: - things.append(File("POTFILES.in", parent, full_text=""" + things.append( + File( + "POTFILES.in", + parent, + full_text=""" [encoding: UTF-8] [type: gettext/rfc822deb] jobs/examples-trivial.txt [type: gettext/rfc822deb] jobs/examples-normal.txt [type: gettext/rfc822deb] jobs/examples-intermediate.txt manage.py - """)) + """, + ) + ) with data_dir as parent: - things.append(File("README.md", parent, full_text=""" + things.append( + File( + "README.md", + parent, + full_text=""" Container for arbitrary data needed by tests ============================================ @@ -491,13 +532,19 @@ class ProviderSkeleton(EmptyProviderSkeleton): You should delete this file as anything here is automatically distributed in the source tarball or installed. - """)) + """, + ) + ) things.append(File("example.dat", parent, full_text="DATA")) with bin_dir as parent: - things.append(File("README.md", parent, full_text=""" + things.append( + File( + "README.md", + parent, + full_text=""" Container for arbitrary executables needed by tests =================================================== @@ -507,12 +554,21 @@ class ProviderSkeleton(EmptyProviderSkeleton): You should delete this file as anything here is automatically distributed in the source tarball or installed. - """)) + """, + ) + ) - things.append(File("custom-executable", parent, True, full_text=""" + things.append( + File( + "custom-executable", + parent, + True, + full_text=""" #!/bin/sh echo "Custom script executed" - """)) + """, + ) + ) things.append(File(".gitignore", full_text="dist/*.tar.gz\nbuild/mo/*\n")) diff --git a/checkbox-ng/checkbox_ng/launcher/subcommands.py b/checkbox-ng/checkbox_ng/launcher/subcommands.py index 855a9f344..aa0053877 100644 --- a/checkbox-ng/checkbox_ng/launcher/subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/subcommands.py @@ -653,11 +653,7 @@ def _start_new_session(self): app_blob["launcher"] = f.read() except FileNotFoundError: pass - self.ctx.sa.update_app_blob( - json.dumps( - app_blob - ).encode("UTF-8") - ) + self.ctx.sa.update_app_blob(json.dumps(app_blob).encode("UTF-8")) bs_jobs = self.ctx.sa.get_bootstrap_todo_list() self._run_bootstrap_jobs(bs_jobs) self.ctx.sa.finish_bootstrap() @@ -1354,9 +1350,9 @@ def invoked(self, ctx): obj = unit._raw_data.copy() obj["unit"] = unit.unit obj["id"] = unit.id # To get the fully qualified id - obj[ - "certification-status" - ] = self.get_effective_certification_status(unit) + obj["certification-status"] = ( + self.get_effective_certification_status(unit) + ) if unit.template_id: obj["template-id"] = unit.template_id obj_list.append(obj) diff --git a/checkbox-ng/checkbox_ng/launcher/test_stages.py b/checkbox-ng/checkbox_ng/launcher/test_stages.py index ac900a185..4028dc18c 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_stages.py +++ b/checkbox-ng/checkbox_ng/launcher/test_stages.py @@ -57,7 +57,7 @@ def test__run_single_job_with_ui_loop_quit_skip_comment(self): # Sequence of user actions: first "comment", then "skip" ui_mock.wait_for_interaction_prompt.side_effect = ["comment", "skip"] # Simulate user entering a comment after being prompted - with mock.patch('builtins.input', return_value="Test comment"): + with mock.patch("builtins.input", return_value="Test comment"): result_builder = MainLoopStage._run_single_job_with_ui_loop( self_mock, job_mock, ui_mock ) diff --git a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py index 9459dbf9b..96297912a 100644 --- a/checkbox-ng/checkbox_ng/launcher/test_subcommands.py +++ b/checkbox-ng/checkbox_ng/launcher/test_subcommands.py @@ -119,9 +119,9 @@ def test__configure_restart( abspath_mock.return_value = "launcher_path" Launcher._configure_restart(tested_self, ctx_mock) - ( - get_restart_cmd_f, - ) = ctx_mock.sa.configure_application_restart.call_args[0] + (get_restart_cmd_f,) = ( + ctx_mock.sa.configure_application_restart.call_args[0] + ) restart_cmd = get_restart_cmd_f("session_id") self.assertEqual( restart_cmd, @@ -142,9 +142,9 @@ def test__configure_restart_snap( abspath_mock.return_value = "launcher_path" Launcher._configure_restart(tested_self, ctx_mock) - ( - get_restart_cmd_f, - ) = ctx_mock.sa.configure_application_restart.call_args[0] + (get_restart_cmd_f,) = ( + ctx_mock.sa.configure_application_restart.call_args[0] + ) restart_cmd = get_restart_cmd_f("session_id") self.assertEqual( restart_cmd, diff --git a/checkbox-ng/checkbox_ng/test_certification.py b/checkbox-ng/checkbox_ng/test_certification.py index 31660e4b7..aac14ce5b 100644 --- a/checkbox-ng/checkbox_ng/test_certification.py +++ b/checkbox-ng/checkbox_ng/test_certification.py @@ -50,19 +50,21 @@ class SubmissionServiceTransportTests(TestCase): valid_option_string = "secure_id={}".format(valid_secure_id) def setUp(self): - self.sample_archive = BytesIO(resource_string( - "plainbox", "test-data/tar-exporter/example-data.tar.xz" - )) - self.patcher = mock.patch('requests.post') + self.sample_archive = BytesIO( + resource_string( + "plainbox", "test-data/tar-exporter/example-data.tar.xz" + ) + ) + self.patcher = mock.patch("requests.post") self.mock_requests = self.patcher.start() def test_parameter_parsing(self): # Makes sense since I'm overriding the base class's constructor. transport = SubmissionServiceTransport( - self.valid_url, self.valid_option_string) + self.valid_url, self.valid_option_string + ) self.assertEqual(self.valid_url, transport.url) - self.assertEqual(self.valid_secure_id, - transport.options['secure_id']) + self.assertEqual(self.valid_secure_id, transport.options["secure_id"]) def test_invalid_length_secure_id_are_rejected(self): length = 14 @@ -78,32 +80,33 @@ def test_invalid_characters_in_secure_id_are_rejected(self): def test_invalid_url(self): transport = SubmissionServiceTransport( - self.invalid_url, self.valid_option_string) + self.invalid_url, self.valid_option_string + ) dummy_data = BytesIO(b"some data to send") requests.post.side_effect = InvalidSchema with self.assertRaises(TransportError): result = transport.send(dummy_data) self.assertIsNotNone(result) - requests.post.assert_called_with( - self.invalid_url, data=dummy_data) + requests.post.assert_called_with(self.invalid_url, data=dummy_data) - @mock.patch('checkbox_ng.certification.logger') + @mock.patch("checkbox_ng.certification.logger") def test_valid_url_cant_connect(self, mock_logger): transport = SubmissionServiceTransport( - self.unreachable_url, self.valid_option_string) + self.unreachable_url, self.valid_option_string + ) dummy_data = BytesIO(b"some data to send") requests.post.side_effect = ConnectionError with self.assertRaises(TransportError): result = transport.send(dummy_data) self.assertIsNotNone(result) - requests.post.assert_called_with(self.unreachable_url, - data=dummy_data) + requests.post.assert_called_with(self.unreachable_url, data=dummy_data) def test_send_success(self): transport = SubmissionServiceTransport( - self.valid_url, self.valid_option_string) - requests.post.return_value = MagicMock(name='response') + self.valid_url, self.valid_option_string + ) + requests.post.return_value = MagicMock(name="response") requests.post.return_value.status_code = 200 requests.post.return_value.text = '{"id": 768}' result = transport.send(self.sample_archive) @@ -111,15 +114,17 @@ def test_send_success(self): def test_send_failure(self): transport = SubmissionServiceTransport( - self.valid_url, self.valid_option_string) - requests.post.return_value = MagicMock(name='response') + self.valid_url, self.valid_option_string + ) + requests.post.return_value = MagicMock(name="response") requests.post.return_value.status_code = 412 - requests.post.return_value.text = 'Some error' + requests.post.return_value.text = "Some error" # Oops, raise_for_status doesn't get fooled by my mocking, # so I have to mock *that* method as well.. response = requests.Response() error = HTTPError(response=response) requests.post.return_value.raise_for_status = MagicMock( - side_effect=error) + side_effect=error + ) with self.assertRaises(TransportError): transport.send(self.sample_archive) diff --git a/checkbox-ng/checkbox_ng/tests.py b/checkbox-ng/checkbox_ng/tests.py index 584b03c54..98dbd1145 100644 --- a/checkbox-ng/checkbox_ng/tests.py +++ b/checkbox-ng/checkbox_ng/tests.py @@ -34,7 +34,7 @@ def load_unit_tests(): """ # Discover all unit tests. By simple convention those are kept in # python modules that start with the word 'test_' . - start_dir = os.path.normpath(os.path.join(get_plainbox_dir(), '..')) + start_dir = os.path.normpath(os.path.join(get_plainbox_dir(), "..")) return defaultTestLoader.discover(start_dir) diff --git a/checkbox-ng/checkbox_ng/user_utils.py b/checkbox-ng/checkbox_ng/user_utils.py index 34d19a281..f796d93dd 100644 --- a/checkbox-ng/checkbox_ng/user_utils.py +++ b/checkbox-ng/checkbox_ng/user_utils.py @@ -2,6 +2,7 @@ This modules has utilities to handle information about users available in the system. """ + import logging import pwd diff --git a/checkbox-ng/plainbox/__init__.py b/checkbox-ng/plainbox/__init__.py index eb93844e5..3f9e09001 100644 --- a/checkbox-ng/plainbox/__init__.py +++ b/checkbox-ng/plainbox/__init__.py @@ -36,19 +36,21 @@ __version__ = version("checkbox-ng") except PackageNotFoundError: import logging - logging.error('Failed to retrieve checkbox-ng version') - __version__ = 'unknown' + + logging.error("Failed to retrieve checkbox-ng version") + __version__ = "unknown" def get_version_string(): import os - version_string = '' - if os.environ.get('SNAP_NAME'): - version_string = '{} {} ({})'.format( - os.environ['SNAP_NAME'], - os.environ.get('SNAP_VERSION', 'unknown_version'), - os.environ.get('SNAP_REVISION', 'unknown_revision') + + version_string = "" + if os.environ.get("SNAP_NAME"): + version_string = "{} {} ({})".format( + os.environ["SNAP_NAME"], + os.environ.get("SNAP_VERSION", "unknown_version"), + os.environ.get("SNAP_REVISION", "unknown_revision"), ) else: - version_string = '{} {}'.format('Checkbox', __version__) + version_string = "{} {}".format("Checkbox", __version__) return version_string diff --git a/checkbox-ng/plainbox/__main__.py b/checkbox-ng/plainbox/__main__.py index 7c28d1bb1..d4a68f0f1 100644 --- a/checkbox-ng/plainbox/__main__.py +++ b/checkbox-ng/plainbox/__main__.py @@ -29,5 +29,5 @@ from plainbox.impl.box import main -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/checkbox-ng/plainbox/abc.py b/checkbox-ng/plainbox/abc.py index 2c74ce7a0..b1bdfc86a 100644 --- a/checkbox-ng/plainbox/abc.py +++ b/checkbox-ng/plainbox/abc.py @@ -197,26 +197,26 @@ class IJobResult(metaclass=ABCMeta): # visible, job outcomes. They can be provided by either automated or manual # "classifier" - a script or a person that clicks a "pass" or "fail" # button. - OUTCOME_PASS = 'pass' - OUTCOME_FAIL = 'fail' + OUTCOME_PASS = "pass" + OUTCOME_FAIL = "fail" # The skip outcome is used when the operator selected a job but then # skipped it. This is typically used for a manual job that is tedious or # was selected by accident. - OUTCOME_SKIP = 'skip' + OUTCOME_SKIP = "skip" # The not supported outcome is used when a job was about to run but a # dependency or resource requirement prevent it from running. XXX: perhaps # this should be called "not available", not supported has the "unsupported # code" feeling associated with it. - OUTCOME_NOT_SUPPORTED = 'not-supported' + OUTCOME_NOT_SUPPORTED = "not-supported" # A temporary state that should be removed later on, used to indicate that # job runner is not implemented but the job "ran" so to speak. - OUTCOME_NOT_IMPLEMENTED = 'not-implemented' + OUTCOME_NOT_IMPLEMENTED = "not-implemented" # A temporary state before the user decides on the outcome of a manual # job or any other job that requires manual verification - OUTCOME_UNDECIDED = 'undecided' + OUTCOME_UNDECIDED = "undecided" # A kind of failed that indicates the underlying test misbehaved. Currently # it is only used when the test program is killed by a signal. - OUTCOME_CRASH = 'crash' + OUTCOME_CRASH = "crash" @abstractproperty def outcome(self): @@ -681,11 +681,11 @@ class IProvider1(IProviderBackend1): @abstractproperty def name(self): - """ - name of this provider + """ + name of this provider - This name should be dbus-friendly. It should not be localizable. - """ + This name should be dbus-friendly. It should not be localizable. + """ @abstractproperty def namespace(self): diff --git a/checkbox-ng/plainbox/i18n.py b/checkbox-ng/plainbox/i18n.py index f13537a3f..1da2ed7f1 100644 --- a/checkbox-ng/plainbox/i18n.py +++ b/checkbox-ng/plainbox/i18n.py @@ -33,16 +33,16 @@ import re __all__ = [ - 'bindtextdomain', - 'dgettext', - 'dngettext', - 'gettext', - 'ngettext', - 'pdgettext', - 'pdngettext', - 'pgettext', - 'pngettext', - 'textdomain', + "bindtextdomain", + "dgettext", + "dngettext", + "gettext", + "ngettext", + "pdgettext", + "pdngettext", + "pgettext", + "pngettext", + "textdomain", ] _logger = logging.getLogger("plainbox.i18n") @@ -254,12 +254,17 @@ def bindtextdomain(self, domain, localedir=None): class LoremIpsumTranslator(NoOpTranslator): LOREM_IPSUM = { - "ch": ('', """小經 施消 了稱 能文 安種 之用 無心 友市 景內 語格。坡對 + "ch": ( + "", + """小經 施消 了稱 能文 安種 之用 無心 友市 景內 語格。坡對 轉醫 題苦 們會員! 我親就 藝了參 間通。 有發 轉前 藥想 亞沒,通須 應管、打者 小成 公出? 般記 中成化 他四華 分國越 分位離,更為者 文難 我如 我布?經動 著為 安經, 們天然 我親 唱顯 - 不;得當 出一來得金 著作 到到 操弟 人望!去指 在格據!"""), - "kr": (' ' """말을 하고 곁에서 일 말려가고 그걸로 하다 같은 없네 + 不;得當 出一來得金 著作 到到 操弟 人望!去指 在格據!""", + ), + "kr": ( + " " + """말을 하고 곁에서 일 말려가고 그걸로 하다 같은 없네 앉은 뿌리치더니 동소문 일 보지 재우쳤다 분량 말을 가지고 김첨지의 시작하였다 내리는 나를 김첨지는 좁쌀 준 반가운지 김첨지는 놓치겠구먼 늦추잡았다 인력거 속 생각하게 돈을 시체를 @@ -268,13 +273,19 @@ class LoremIpsumTranslator(NoOpTranslator): 돈의 라고 어이 없지만 받아야 아내의 시작하였다 차도 왜 사용자로부터 추어탕을 처음 보라 출판사 차원 따라서 펴서 풀이 사람은 근심과 초조해온다 트고 제 창을 내리었다 인력거하고 - 같으면 큰 이놈아 어린애 그 넘어 울었다V"""), - "he": (' ', """תורת קרימינולוגיה אל אתה הטבע לחיבור אם אחר מדע חינוך + 같으면 큰 이놈아 어린애 그 넘어 울었다V""" + ), + "he": ( + " ", + """תורת קרימינולוגיה אל אתה הטבע לחיבור אם אחר מדע חינוך ממונרכיה גם פנאי אחרים המקובל את אתה תנך אחרים לטיפול של את תיאטרון ואלקטרוניקה מתן דת והנדסה שימושיים סדר בה סרבול אינטרנט שתי ב אנא תוכל לערך רוסית כדי את תוכל כניסה המלחמה - עוד מה מיזמי אודות ומהימנה"""), - "ar": (' ', """ دار أن منتصف أوراقهم الرئيسية هو الا الحرب الجبهة لان + עוד מה מיזמי אודות ומהימנה""", + ), + "ar": ( + " ", + """ دار أن منتصف أوراقهم الرئيسية هو الا الحرب الجبهة لان مع تنفّس للصين لإنعدام نتيجة الثقيلة أي شيء عقبت وأزيز لألمانيا وفي كل حدى إختار المنتصرة أي به، بغزو بالسيطرة أن جدول بالفشل إيطاليا قام كل هنا؟ فرنسا الهجوم هذه مع حقول @@ -283,21 +294,30 @@ class LoremIpsumTranslator(NoOpTranslator): فعل فاتّبع الشّعبين المعركة، ما الى ما يطول المشتّتون وكسبت وإيطالي ذات أم تلك ثم القصف قبضتهم قد وأزيز إستمات ونستون غزو الأرض الأولية عن بين بـ دفّة كانت النفط لمّ تلك فهرست الأرض - الإتفاقية مع"""), - "ru": (' ', """Магна азжюывырит мэль ут нам ыт видырэр такематыш кибо + الإتفاقية مع""", + ), + "ru": ( + " ", + """Магна азжюывырит мэль ут нам ыт видырэр такематыш кибо ыррор ут квюо Вяш аппарэат пондэрюм интылльэгэбат эи про ед еллум дикунт Квюо экз льаборэж нужквюам анкилльаы мэль омйттам мэнандря ед Мэль эи рэктэквуэ консэквюат контынтёонэж ты ёужто фэугяат вивэндюм шэа Атквюе трётанё эю квуй омнеж латины экз - вимi"""), - "jp": ('', """戸ぶだ の意 化巡奇 供 クソリヤ 無断 ヨサリヲ 念休ばイ + вимi""", + ), + "jp": ( + "", + """戸ぶだ の意 化巡奇 供 クソリヤ 無断 ヨサリヲ 念休ばイ 例会 コトヤ 耕智う ばっゃ 佐告決う で打表 ぞ ぼび情記ト レ表関銀 ロモア ニ次川 よ全子 コロフ ソ政象 住岳ぴ 読ワ 一針 ヘ断 首画リ のぽ せ足 決属 術こ てラ 領 技 けリぴ 分率ぴ きぜっ 物味ドン おぎ一田ぴ ぶの謙 調ヲ星度 レぼむ囲 舗双脈 鶴挑げ ほぶ。無無 ツ縄第が 本公作 ゅゃふ く質失フ 米上議 ア記治 えれ本 - 意つん ぎレ局 総ケ盛 載テ コ部止 メツ輪 帰歴 就些ル っき"""), - "pl": (' ', """ + 意つん ぎレ局 総ケ盛 載テ コ部止 メツ輪 帰歴 就些ル っき""", + ), + "pl": ( + " ", + """ litwo ojczyzno moja ty jesteś jak zdrowie ile cię stracił dziś piękność widziana więc wszyscy dokoła brali stronę kusego albo sam wewnątrz siebie czuł się położył co by stary @@ -316,7 +336,8 @@ class LoremIpsumTranslator(NoOpTranslator): całe wesoło lecz go grzecznie na złość rejentowi że u wieczerzy będzie jego upadkiem domy i bagnami skradał się tłocz i jak bawić się nie było bo tak na jutro solwuję i przepraszał - sędziego sędzia sam na początek dać małą kiedy"""), + sędziego sędzia sam na początek dać małą kiedy""", + ), } def __init__(self, kind): @@ -329,9 +350,10 @@ def __init__(self, kind): def _get_ipsum(self, text): return re.sub( - '(%[sdr]|{[^}]*}|[a-zA-Z]+)', + "(%[sdr]|{[^}]*}|[a-zA-Z]+)", lambda match: self._tr_word(match.group(1)), - text) + text, + ) def _tr_word(self, word): if re.search("(%[sdr])|({[^}]*})", word): @@ -383,9 +405,7 @@ class GettextTranslator(ITranslator): def __init__(self, domain, locale_dir=None): self._domain = domain self._translations = {} - self._locale_dir_map = { - domain: locale_dir - } + self._locale_dir_map = {domain: locale_dir} def _get_translation(self, domain): try: @@ -393,7 +413,8 @@ def _get_translation(self, domain): except KeyError: try: translation = gettext_module.translation( - domain, self._locale_dir_map.get(domain)) + domain, self._locale_dir_map.get(domain) + ) except IOError: translation = gettext_module.NullTranslations() self._translations[domain] = translation @@ -466,7 +487,8 @@ def pdngettext(self, msgctxt, domain, msgid1, msgid2, n): effective_msgid1 = self._contextualize(msgctxt, msgid1) effective_msgid2 = self._contextualize(msgctxt, msgid2) msgstr = self._get_translation(domain).ngettext( - effective_msgid1, effective_msgid2, n) + effective_msgid1, effective_msgid2, n + ) # If we got the untranslated version then we want to just return msgid1 # or msgid2 back, without msgctxt prepended in front. if msgstr == effective_msgid1: @@ -527,6 +549,7 @@ def foo(): class Foo: pass """ + def decorator(cls_or_func): try: cls_or_func.__doc__ = docstring @@ -534,9 +557,9 @@ def decorator(cls_or_func): except AttributeError: assert isinstance(cls_or_func, type) return type( - cls_or_func.__name__, - (cls_or_func,), - {'__doc__': docstring}) + cls_or_func.__name__, (cls_or_func,), {"__doc__": docstring} + ) + return decorator @@ -561,7 +584,8 @@ def gettext_noop(msgid): try: _translator = { "gettext": GettextTranslator( - "plainbox", os.getenv("PLAINBOX_LOCALE_DIR", None)), + "plainbox", os.getenv("PLAINBOX_LOCALE_DIR", None) + ), "no-op": NoOpTranslator(), "lorem-ipsum-ar": LoremIpsumTranslator("ar"), "lorem-ipsum-ch": LoremIpsumTranslator("ch"), @@ -573,7 +597,8 @@ def gettext_noop(msgid): }[os.getenv("PLAINBOX_I18N_MODE", "gettext")] except KeyError as exc: raise RuntimeError( - "Unsupported PLAINBOX_I18N_MODE: {!r}".format(exc.args[0])) + "Unsupported PLAINBOX_I18N_MODE: {!r}".format(exc.args[0]) + ) # This is the public API of this module diff --git a/checkbox-ng/plainbox/impl/__init__.py b/checkbox-ng/plainbox/impl/__init__.py index 7f022894f..35e3df054 100644 --- a/checkbox-ng/plainbox/impl/__init__.py +++ b/checkbox-ng/plainbox/impl/__init__.py @@ -40,7 +40,7 @@ def _get_doc_margin(doc): """ Find minimum indentation of any non-blank lines after first line. """ - lines = doc.expandtabs().split('\n') + lines = doc.expandtabs().split("\n") margin = sys.maxsize for line in lines[1:]: content = len(line.lstrip()) @@ -76,6 +76,7 @@ def public(import_path, introduced=None, deprecated=None): by import_path. It can be a module name or a module name and a function name, when separated by a colon. """ + # Create a forwarding decorator for the shim function. The shim argument is # the actual empty function from the public module that serves as # documentation carrier. @@ -87,49 +88,67 @@ def decorator(shim): except ValueError: module_name, func_name = import_path, shim.__name__ # Import the module with the implementation and extract the function - module = __import__(module_name, fromlist=['']) + module = __import__(module_name, fromlist=[""]) try: impl = getattr(module, func_name) except AttributeError: raise NotImplementedError( - "%s.%s does not exist" % (module_name, func_name)) + "%s.%s does not exist" % (module_name, func_name) + ) @wraps(shim) def call_impl(*args, **kwargs): return impl(*args, **kwargs) + # Document the public nature of the function - call_impl.__doc__ += "\n".join([ - "", - " This function is a part of the public API", - " The private implementation is in {}:{}".format( - import_path, shim.__name__) - ]) - if introduced is None: - call_impl.__doc__ += "\n".join([ + call_impl.__doc__ += "\n".join( + [ "", - " This function was introduced in the initial version of" - " plainbox", - ]) + " This function is a part of the public API", + " The private implementation is in {}:{}".format( + import_path, shim.__name__ + ), + ] + ) + if introduced is None: + call_impl.__doc__ += "\n".join( + [ + "", + " This function was introduced in the initial version of" + " plainbox", + ] + ) else: - call_impl.__doc__ += "\n".join([ - "", - " This function was introduced in version: {}".format( - introduced) - ]) + call_impl.__doc__ += "\n".join( + [ + "", + " This function was introduced in version: {}".format( + introduced + ), + ] + ) # Document deprecation status, if any if deprecated is not None: - call_impl.__doc__ += "\n".join([ - " warn:", - " This function is deprecated", - " It will be removed in version: {}".format(deprecated), - ]) + call_impl.__doc__ += "\n".join( + [ + " warn:", + " This function is deprecated", + " It will be removed in version: {}".format( + deprecated + ), + ] + ) # Add implementation docs, if any if impl.__doc__ is not None: - call_impl.__doc__ += "\n".join([ - " Additional documentation from the private" - " implementation:"]) + call_impl.__doc__ += "\n".join( + [ + " Additional documentation from the private" + " implementation:" + ] + ) call_impl.__doc__ += impl.__doc__ return call_impl + return decorator @@ -161,22 +180,25 @@ def decorator(func): The @deprecated decorator with deprecation information """ msg = "{0} is deprecated since version {1}".format( - func.__name__, version) + func.__name__, version + ) if func.__doc__ is None: - func.__doc__ = '' - indent = 4 * ' ' + func.__doc__ = "" + indent = 4 * " " else: - indent = _get_doc_margin(func.__doc__) * ' ' - func.__doc__ += indent + '\n' - func.__doc__ += indent + '.. deprecated:: {}'.format(version) + indent = _get_doc_margin(func.__doc__) * " " + func.__doc__ += indent + "\n" + func.__doc__ += indent + ".. deprecated:: {}".format(version) if explanation is not None: func.__doc__ += _textwrap_indent( - textwrap.dedent(explanation), prefix=indent * 2) + textwrap.dedent(explanation), prefix=indent * 2 + ) @wraps(func) def wrapper(*args, **kwargs): warn(DeprecationWarning(msg), stacklevel=2) return func(*args, **kwargs) + return wrapper return decorator diff --git a/checkbox-ng/plainbox/impl/_argparse.py b/checkbox-ng/plainbox/impl/_argparse.py index ea0c63e96..0308bc02f 100644 --- a/checkbox-ng/plainbox/impl/_argparse.py +++ b/checkbox-ng/plainbox/impl/_argparse.py @@ -136,7 +136,7 @@ def _format_text(self, text): def _format_usage(self, usage, actions, groups, prefix): if prefix is None: - prefix = argparse._('usage: ') + prefix = argparse._("usage: ") # if usage is specified, use that if usage is not None: @@ -144,11 +144,11 @@ def _format_usage(self, usage, actions, groups, prefix): # if no optionals or positionals are available, usage is just prog elif usage is None and not actions: - usage = '%(prog)s' % dict(prog=self._prog) + usage = "%(prog)s" % dict(prog=self._prog) # if optionals and positionals are available, calculate usage elif usage is None: - prog = '%(prog)s' % dict(prog=self._prog) + prog = "%(prog)s" % dict(prog=self._prog) # split optionals from positionals optionals = [] @@ -162,20 +162,20 @@ def _format_usage(self, usage, actions, groups, prefix): # build full usage string format = self._format_actions_usage action_usage = format(optionals + positionals, groups) - usage = ' '.join([s for s in [prog, action_usage] if s]) + usage = " ".join([s for s in [prog, action_usage] if s]) # wrap the usage parts if it's too long text_width = self._width - self._current_indent if len(prefix) + len(usage) > text_width: # break usage into wrappable parts - part_regexp = r'\(.*?\)+|\[.*?\]+|\S+' + part_regexp = r"\(.*?\)+|\[.*?\]+|\S+" opt_usage = format(optionals, groups) pos_usage = format(positionals, groups) opt_parts = argparse._re.findall(part_regexp, opt_usage) pos_parts = argparse._re.findall(part_regexp, pos_usage) - assert ' '.join(opt_parts) == opt_usage - assert ' '.join(pos_parts) == pos_usage + assert " ".join(opt_parts) == opt_usage + assert " ".join(pos_parts) == pos_usage # helper for wrapping lines def get_lines(parts, indent, prefix=None): @@ -187,20 +187,20 @@ def get_lines(parts, indent, prefix=None): line_len = len(indent) - 1 for part in parts: if line_len + 1 + len(part) > text_width: - lines.append(indent + ' '.join(line)) + lines.append(indent + " ".join(line)) line = [] line_len = len(indent) - 1 line.append(part) line_len += len(part) + 1 if line: - lines.append(indent + ' '.join(line)) + lines.append(indent + " ".join(line)) if prefix is not None: - lines[0] = lines[0][len(indent):] + lines[0] = lines[0][len(indent) :] return lines # if prog is short, follow it with optionals or positionals if len(prefix) + len(prog) <= 0.75 * text_width: - indent = ' ' * (len(prefix) + len(prog) + 1) + indent = " " * (len(prefix) + len(prog) + 1) if opt_parts: lines = get_lines([prog] + opt_parts, indent, prefix) lines.extend(get_lines(pos_parts, indent)) @@ -211,7 +211,7 @@ def get_lines(parts, indent, prefix=None): # if prog is long, put it on its own line else: - indent = ' ' * len(prefix) + indent = " " * len(prefix) parts = opt_parts + pos_parts lines = get_lines(parts, indent) if len(lines) > 1: @@ -221,7 +221,7 @@ def get_lines(parts, indent, prefix=None): lines = [prog] + lines # join lines into usage - usage = '\n'.join(lines) + usage = "\n".join(lines) # prefix with 'usage:' - return '%s%s\n\n' % (prefix, usage) + return "%s%s\n\n" % (prefix, usage) diff --git a/checkbox-ng/plainbox/impl/_shlex.py b/checkbox-ng/plainbox/impl/_shlex.py index 5b943ed5e..40b9c2b3e 100644 --- a/checkbox-ng/plainbox/impl/_shlex.py +++ b/checkbox-ng/plainbox/impl/_shlex.py @@ -7,7 +7,7 @@ import re -_find_unsafe = re.compile(r'[^\w@%+=:,./-]', re.ASCII).search +_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search def quote(s): diff --git a/checkbox-ng/plainbox/impl/_textwrap.py b/checkbox-ng/plainbox/impl/_textwrap.py index c356bc179..d8300387c 100644 --- a/checkbox-ng/plainbox/impl/_textwrap.py +++ b/checkbox-ng/plainbox/impl/_textwrap.py @@ -85,10 +85,12 @@ def _textwrap_indent(text, prefix, predicate=None): consist solely of whitespace characters. """ if predicate is None: + def predicate(line): return line.strip() def prefixed_lines(): for line in text.splitlines(True): yield (prefix + line if predicate(line) else line) - return ''.join(prefixed_lines()) + + return "".join(prefixed_lines()) diff --git a/checkbox-ng/plainbox/impl/box.py b/checkbox-ng/plainbox/impl/box.py index b9fb342e9..edd094f56 100644 --- a/checkbox-ng/plainbox/impl/box.py +++ b/checkbox-ng/plainbox/impl/box.py @@ -46,12 +46,27 @@ class PlainBoxTool(LazyLoadingToolMixIn, PlainBoxToolBase): def get_command_collection(self): p = "plainbox.impl.commands." - return LazyPlugInCollection(collections.OrderedDict([ - ('session', (p + "cmd_session:SessionCommand", - self._load_providers)), - ('dev', (p + "dev:DevCommand", self._load_providers, - self._load_config)), - ])) + return LazyPlugInCollection( + collections.OrderedDict( + [ + ( + "session", + ( + p + "cmd_session:SessionCommand", + self._load_providers, + ), + ), + ( + "dev", + ( + p + "dev:DevCommand", + self._load_providers, + self._load_config, + ), + ), + ] + ) + ) @classmethod def get_exec_name(cls): @@ -72,8 +87,9 @@ def create_parser_object(self): parser.prog = self.get_exec_name() # TRANSLATORS: '--help' and '--version' are not translatable, # but '[options]' and '' are. - parser.usage = _("{0} [--help] [--version] | [options] " - " ...").format(self.get_exec_name()) + parser.usage = _( + "{0} [--help] [--version] | [options] " " ..." + ).format(self.get_exec_name()) return parser @classmethod diff --git a/checkbox-ng/plainbox/impl/buildsystems.py b/checkbox-ng/plainbox/impl/buildsystems.py index 8eabeb64a..393fa3623 100644 --- a/checkbox-ng/plainbox/impl/buildsystems.py +++ b/checkbox-ng/plainbox/impl/buildsystems.py @@ -47,8 +47,10 @@ def probe(self, src_dir: str) -> int: def get_build_command(self, src_dir: str, build_dir: str) -> str: return "VPATH={} make -f {}".format( shlex.quote(os.path.relpath(src_dir, build_dir)), - shlex.quote(os.path.relpath( - os.path.join(src_dir, 'Makefile'), build_dir))) + shlex.quote( + os.path.relpath(os.path.join(src_dir, "Makefile"), build_dir) + ), + ) class AutotoolsBuildSystem(IBuildSystem): @@ -63,7 +65,8 @@ def probe(self, src_dir: str) -> int: def get_build_command(self, src_dir: str, build_dir: str) -> str: return "{}/configure && make".format( - shlex.quote(os.path.relpath(src_dir, build_dir))) + shlex.quote(os.path.relpath(src_dir, build_dir)) + ) class GoBuildSystem(IBuildSystem): @@ -81,4 +84,4 @@ def get_build_command(self, src_dir: str, build_dir: str) -> str: # Collection of all buildsystems -all_buildsystems = PkgResourcesPlugInCollection('plainbox.buildsystem') +all_buildsystems = PkgResourcesPlugInCollection("plainbox.buildsystem") diff --git a/checkbox-ng/plainbox/impl/clitools.py b/checkbox-ng/plainbox/impl/clitools.py index d440e718c..5f156baf7 100644 --- a/checkbox-ng/plainbox/impl/clitools.py +++ b/checkbox-ng/plainbox/impl/clitools.py @@ -123,7 +123,8 @@ def get_localized_docstring(self): """ if self.__class__.__doc__ is not None: return inspect.cleandoc( - dgettext(self.get_gettext_domain(), self.__class__.__doc__)) + dgettext(self.get_gettext_domain(), self.__class__.__doc__) + ) def get_command_help(self): """ @@ -168,9 +169,11 @@ def get_command_description(self): except AttributeError: pass try: - return '\n'.join( - self.get_localized_docstring().splitlines()[1:] - ).split('@EPILOG@', 1)[0].strip() + return ( + "\n".join(self.get_localized_docstring().splitlines()[1:]) + .split("@EPILOG@", 1)[0] + .strip() + ) except (AttributeError, IndexError, ValueError): pass @@ -194,9 +197,11 @@ def get_command_epilog(self): except AttributeError: pass try: - return '\n'.join( - self.get_localized_docstring().splitlines()[1:] - ).split('@EPILOG@', 1)[1].strip() + return ( + "\n".join(self.get_localized_docstring().splitlines()[1:]) + .split("@EPILOG@", 1)[1] + .strip() + ) except (AttributeError, IndexError, ValueError): pass @@ -234,8 +239,12 @@ def add_subcommand(self, subparsers): epilog = self.get_command_epilog() name = self.get_command_name() parser = subparsers.add_parser( - name, help=help, description=description, epilog=epilog, - formatter_class=argparse.RawDescriptionHelpFormatter) + name, + help=help, + description=description, + epilog=epilog, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) parser.set_defaults(command=self) return parser @@ -284,7 +293,8 @@ def _setup_logging_from_environment(self): adjust_logging( level=os.getenv("PLAINBOX_LOG_LEVEL", "DEBUG"), trace_list=os.getenv("PLAINBOX_TRACE", "").split(","), - debug_console=os.getenv("PLAINBOX_DEBUG", "") == "console") + debug_console=os.getenv("PLAINBOX_DEBUG", "") == "console", + ) logger.debug(_("Activated early logging via environment variables")) def main(self, argv=None): @@ -302,7 +312,8 @@ def main(self, argv=None): logger.debug(_("Parsing command line arguments (early mode)")) early_ns = self._early_parser.parse_args(argv) logger.debug( - _("Command line parsed to (early mode): %r"), early_ns) + _("Command line parsed to (early mode): %r"), early_ns + ) logger.debug(_("Tool initialization (late mode)")) self.late_init(early_ns) # Construct the full command line argument parser @@ -412,8 +423,10 @@ def late_init(self, early_ns): Initialize with early command line arguments being already parsed """ adjust_logging( - level=early_ns.log_level, trace_list=early_ns.trace, - debug_console=early_ns.debug_console) + level=early_ns.log_level, + trace_list=early_ns.trace, + debug_console=early_ns.debug_console, + ) def final_init(self, ns): """ @@ -451,13 +464,16 @@ def create_parser_object(self): argparse.ArgumentParser instance. """ parser = argparse.ArgumentParser( - prog=self.get_exec_name(), - formatter_class=LegacyHelpFormatter) + prog=self.get_exec_name(), formatter_class=LegacyHelpFormatter + ) # NOTE: help= is provided explicitly as argparse doesn't wrap # everything with _() correctly (depending on version) parser.add_argument( - "--version", action="version", version=self.get_exec_version(), - help=_("show program's version number and exit")) + "--version", + action="version", + version=self.get_exec_version(), + help=_("show program's version number and exit"), + ) return parser def construct_parser(self, early_ns=None): @@ -480,61 +496,76 @@ def enable_argcomplete_if_possible(self, parser): argcomplete.autocomplete(parser) def add_early_parser_arguments(self, parser): - group = parser.add_argument_group( - title=_("logging and debugging")) + group = parser.add_argument_group(title=_("logging and debugging")) # Add the --log-level argument group.add_argument( - "-l", "--log-level", + "-l", + "--log-level", action="store", - choices=('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), + choices=("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), default=None, - help=argparse.SUPPRESS) + help=argparse.SUPPRESS, + ) # Add the --verbose argument group.add_argument( - "-v", "--verbose", + "-v", + "--verbose", dest="log_level", action="store_const", const="INFO", # TRANSLATORS: please keep --log-level=INFO untranslated - help=_("be more verbose (same as --log-level=INFO)")) + help=_("be more verbose (same as --log-level=INFO)"), + ) # Add the --debug flag group.add_argument( - "-D", "--debug", + "-D", + "--debug", dest="log_level", action="store_const", const="DEBUG", # TRANSLATORS: please keep DEBUG untranslated - help=_("enable DEBUG messages on the root logger")) + help=_("enable DEBUG messages on the root logger"), + ) # Add the --debug flag group.add_argument( - "-C", "--debug-console", + "-C", + "--debug-console", action="store_true", # TRANSLATORS: please keep DEBUG untranslated - help=_("display DEBUG messages in the console")) + help=_("display DEBUG messages in the console"), + ) # Add the --trace flag group.add_argument( - "-T", "--trace", + "-T", + "--trace", metavar=_("LOGGER"), action="append", default=[], # TRANSLATORS: please keep DEBUG untranslated - help=_("enable DEBUG messages on the specified logger " - "(can be used multiple times)")) + help=_( + "enable DEBUG messages on the specified logger " + "(can be used multiple times)" + ), + ) # Add the --pdb flag group.add_argument( - "-P", "--pdb", + "-P", + "--pdb", action="store_true", default=False, # TRANSLATORS: please keep pdb untranslated - help=_("jump into pdb (python debugger) when a command crashes")) + help=_("jump into pdb (python debugger) when a command crashes"), + ) # Add the --debug-interrupt flag group.add_argument( - "-I", "--debug-interrupt", + "-I", + "--debug-interrupt", action="store_true", default=False, # TRANSLATORS: please keep SIGINT/KeyboardInterrupt and --pdb # untranslated - help=_("crash on SIGINT/KeyboardInterrupt, useful with --pdb")) + help=_("crash on SIGINT/KeyboardInterrupt, useful with --pdb"), + ) def dispatch_command(self, ns): # Argh the horrror! @@ -549,8 +580,10 @@ def dispatch_command(self, ns): # To compensate, on python3.3 and beyond, when the user just runs # plainbox without specifying the command, we manually, explicitly do # what python3.2 did: call parser.error(_('too few arguments')) - if (sys.version_info[:2] >= (3, 3) - and getattr(ns, "command", None) is None): + if ( + sys.version_info[:2] >= (3, 3) + and getattr(ns, "command", None) is None + ): self._parser.error(argparse._("too few arguments")) else: return ns.command.invoked(ns) @@ -572,31 +605,34 @@ def dispatch_and_catch_exceptions(self, ns): # For all other exceptions (and I mean all), do a few checks # and perform actions depending on the command line arguments # By default we want to re-raise the exception - action = 'raise' + action = "raise" # We want to ignore IOErrors that are really EPIPE if isinstance(exc, IOError): if exc.errno == errno.EPIPE: - action = 'ignore' + action = "ignore" # We want to ignore KeyboardInterrupt unless --debug-interrupt # was passed on command line elif isinstance(exc, KeyboardInterrupt): if ns.debug_interrupt: - action = 'debug' + action = "debug" else: - action = 'ignore' + action = "ignore" else: # For all other execptions, debug if requested if ns.pdb: - action = 'debug' + action = "debug" logger.debug(_("action for exception %r is %s"), exc, action) - if action == 'ignore': + if action == "ignore": return 0 - elif action == 'raise': + elif action == "raise": logging.getLogger("plainbox.crashes").fatal( _("Executable %r invoked with %r has crashed"), - self.get_exec_name(), ns, exc_info=1) + self.get_exec_name(), + ns, + exc_info=1, + ) raise - elif action == 'debug': + elif action == "debug": logger.error(_("caught runaway exception: %r"), exc) logger.error(_("starting debugger...")) pdb.post_mortem() @@ -637,7 +673,7 @@ def get_command_collection(self) -> IPlugInCollection: def add_subcommands( self, subparsers: argparse._SubParsersAction, - early_ns: "Maybe[argparse.Namespace]"=None, + early_ns: "Maybe[argparse.Namespace]" = None, ) -> None: """ Add top-level subcommands to the argument parser. @@ -659,11 +695,11 @@ def add_subcommands( self.add_subcommands_with_hints(subparsers, early_ns.rest) else: self.add_subcommands_without_hints( - subparsers, self.get_command_collection()) + subparsers, self.get_command_collection() + ) def add_subcommands_with_hints( - self, subparsers: argparse._SubParsersAction, - hint_list: "List[str]" + self, subparsers: argparse._SubParsersAction, hint_list: "List[str]" ) -> None: """ Add top-level subcommands to the argument parser, using a list of @@ -692,11 +728,12 @@ def add_subcommands_with_hints( :meth:`get_command_collection()` """ logger.debug( - _("Trying to load exactly the right command: %r"), hint_list) + _("Trying to load exactly the right command: %r"), hint_list + ) command_collection = self.get_command_collection() for hint in hint_list: # Skip all the things that look like additional options - if hint.startswith('-'): + if hint.startswith("-"): continue # Break on the first hint that we can load try: @@ -708,15 +745,17 @@ def add_subcommands_with_hints( logger.debug("Registering single command %r", command) start = now() command.register_parser(subparsers) - logger.debug(_("Cost of registering guessed command: %f"), - now() - start) + logger.debug( + _("Cost of registering guessed command: %f"), now() - start + ) break else: logger.debug("Falling back to loading all commands") self.add_subcommands_without_hints(subparsers, command_collection) def add_subcommands_without_hints( - self, subparsers: argparse._SubParsersAction, + self, + subparsers: argparse._SubParsersAction, command_collection: IPlugInCollection, ) -> None: """ @@ -740,14 +779,15 @@ def add_subcommands_without_hints( command_collection.load() logger.debug( _("Cost of loading all top-level commands: %f"), - command_collection.get_total_time()) + command_collection.get_total_time(), + ) start = now() for command in command_collection.get_all_plugin_objects(): logger.debug("Registering command %r", command) command.register_parser(subparsers) logger.debug( - _("Cost of registering all top-level commands: %f"), - now() - start) + _("Cost of registering all top-level commands: %f"), now() - start + ) class SingleCommandToolMixIn: @@ -803,7 +843,7 @@ def customize_parser(self, parser): cmd.register_arguments(parser) -def autopager(pager_list=['sensible-pager', 'less', 'more']): +def autopager(pager_list=["sensible-pager", "less", "more"]): """ Enable automatic pager @@ -841,7 +881,7 @@ def autopager(pager_list=['sensible-pager', 'less', 'more']): return # Check if the user has a PAGER set, if so, consider that the prime # candidate for the effective pager. - pager = os.getenv('PAGER') + pager = os.getenv("PAGER") if pager is not None: pager_list = [pager] + pager_list # Find the best pager based on user preferences and built-in knowledge @@ -896,4 +936,6 @@ def find_exec(name_list): return (name, pathname) raise LookupError( _("Unable to find any of the executables {}").format( - ", ".join(name_list))) + ", ".join(name_list) + ) + ) diff --git a/checkbox-ng/plainbox/impl/color.py b/checkbox-ng/plainbox/impl/color.py index e7006064f..8a7c66657 100644 --- a/checkbox-ng/plainbox/impl/color.py +++ b/checkbox-ng/plainbox/impl/color.py @@ -34,6 +34,7 @@ class f: """ Foreground color attributes """ + BLACK = 30 RED = 31 GREEN = 32 @@ -49,6 +50,7 @@ class b: """ Background color attributes """ + BLACK = 40 RED = 41 GREEN = 42 @@ -64,6 +66,7 @@ class s: """ Style attributes """ + BRIGHT = 1 DIM = 2 NORMAL = 22 @@ -84,8 +87,8 @@ class s: # Convert from numbers to full escape sequences for obj_on, obj_off in zip( - (ansi_on.f, ansi_on.b, ansi_on.s), - (ansi_off.f, ansi_off.b, ansi_off.s)): + (ansi_on.f, ansi_on.b, ansi_on.s), (ansi_off.f, ansi_off.b, ansi_off.s) +): for name in [name for name in dir(obj_on) if name.isupper()]: setattr(obj_on, name, "\033[%sm" % getattr(obj_on, name)) setattr(obj_off, name, "") @@ -134,10 +137,9 @@ def is_enabled(self): return self.c is ansi_on def result(self, result): - return self.custom( - result.tr_outcome(), result.outcome_color_ansi()) + return self.custom(result.tr_outcome(), result.outcome_color_ansi()) - def header(self, text, color_name='WHITE', bright=True, fill='='): + def header(self, text, color_name="WHITE", bright=True, fill="="): return self("[ {} ]".format(text).center(80, fill), color_name, bright) def f(self, color_name): @@ -150,10 +152,14 @@ def s(self, style_name): return getattr(self.c.s, style_name.upper()) def __call__(self, text, color_name="WHITE", bright=True): - return ''.join([ - self.f(color_name), - self.c.s.BRIGHT if bright else '', str(text), - self.c.s.RESET_ALL]) + return "".join( + [ + self.f(color_name), + self.c.s.BRIGHT if bright else "", + str(text), + self.c.s.RESET_ALL, + ] + ) def custom(self, text, ansi_code): """ @@ -173,10 +179,9 @@ def custom(self, text, ansi_code): done to ensure that any custom styling is not permantently enabled if colors are to be disabled. """ - return ''.join([ - ansi_code if self.is_enabled else "", - text, - self.c.s.RESET_ALL]) + return "".join( + [ansi_code if self.is_enabled else "", text, self.c.s.RESET_ALL] + ) def BLACK(self, text, bright=True): return self(text, "BLACK", bright) @@ -204,7 +209,6 @@ def WHITE(self, text, bright=True): class CanonicalColors: - """ Canonical Color Palette. @@ -270,23 +274,23 @@ class CanonicalColors: """ #: Ubuntu orange color - ubuntu_orange = (0xdd, 0x48, 0x14) + ubuntu_orange = (0xDD, 0x48, 0x14) #: White color - white = (0xff, 0xff, 0xff) + white = (0xFF, 0xFF, 0xFF) #: Black color black = (0x00, 0x00, 0x00) #: Light aubergine color - light_aubergine = (0x77, 0x21, 0x6f) + light_aubergine = (0x77, 0x21, 0x6F) #: Mid aubergine color - mid_aubergine = (0x5e, 0x27, 0x50) + mid_aubergine = (0x5E, 0x27, 0x50) #: Dark aubergine color - dark_aubergine = (0x2c, 0x00, 0x1e) + dark_aubergine = (0x2C, 0x00, 0x1E) #: Warm grey color - warm_grey = (0xae, 0xa7, 0x9f) + warm_grey = (0xAE, 0xA7, 0x9F) #: Cool grey color cool_grey = (0x33, 0x33, 0x33) #: Color for small grey dots - small_dot_grey = (0xae, 0xa7, 0x9f) + small_dot_grey = (0xAE, 0xA7, 0x9F) #: Canonical aubergine color canonical_aubergine = (0x77, 0x29, 0x53) #: Text gray color diff --git a/checkbox-ng/plainbox/impl/commands/cmd_parse.py b/checkbox-ng/plainbox/impl/commands/cmd_parse.py index 76f4e763a..7621efe0d 100644 --- a/checkbox-ng/plainbox/impl/commands/cmd_parse.py +++ b/checkbox-ng/plainbox/impl/commands/cmd_parse.py @@ -34,11 +34,12 @@ def __init__(self): def invoked(self, ns): self.parser_collection.load() - if ns.parser_name == '?': + if ns.parser_name == "?": return self._print_parser_list() else: parser = self.parser_collection.get_by_name(ns.parser_name) from plainbox.impl.commands.inv_parse import ParseInvocation + return ParseInvocation(parser).run() def _print_parser_list(self): @@ -49,10 +50,12 @@ def _print_parser_list(self): def register_parser(self, subparsers): parser = subparsers.add_parser( - "parse", help=_("parse stdin with the specified parser"), + "parse", + help=_("parse stdin with the specified parser"), prog="plainbox dev parse", # TRANSLATORS: please keep plainbox.parsers untranslated. - description=_(""" + description=_( + """ This command can be used to invoke any of the parsers exposed to the `plainbox.parsers` entry point, parse standard input and produce a JSON dump of the resulting data structure on stdout. @@ -60,13 +63,18 @@ def register_parser(self, subparsers): Keep in mind that most parsers were designed with the 'C' locale in mind. You may have to override the environment variable LANG to "C". - """), - epilog=(_("Example: ") - + "LANG=C pactl list | plainbox dev parse pactl-list"), + """ + ), + epilog=( + _("Example: ") + + "LANG=C pactl list | plainbox dev parse pactl-list" + ), ) parser.set_defaults(command=self) self.parser_collection.load() parser.add_argument( - "parser_name", metavar=_("PARSER-NAME"), - choices=['?'] + list(self.parser_collection.get_all_names()), - help=_("Name of the parser to use")) + "parser_name", + metavar=_("PARSER-NAME"), + choices=["?"] + list(self.parser_collection.get_all_names()), + help=_("Name of the parser to use"), + ) diff --git a/checkbox-ng/plainbox/impl/commands/cmd_session.py b/checkbox-ng/plainbox/impl/commands/cmd_session.py index c7b1cd615..cd8d10daa 100644 --- a/checkbox-ng/plainbox/impl/commands/cmd_session.py +++ b/checkbox-ng/plainbox/impl/commands/cmd_session.py @@ -29,7 +29,8 @@ @docstring( - N_(""" + N_( + """ session management commands This command can be used to list, show and remove sessions owned by the @@ -47,7 +48,9 @@ used by applications): incomplete and submitted. The 'incomplete' flag is removed after all desired jobs have been executed. The 'submitted' flag is set after a submission is made using any of the transport mechanisms. - """)) + """ + ) +) class SessionCommand(PlainBoxCommand): def __init__(self, provider_loader): @@ -56,72 +59,120 @@ def __init__(self, provider_loader): def invoked(self, ns): from plainbox.impl.commands.inv_session import SessionInvocation + return SessionInvocation(ns, self.provider_loader).run() def register_parser(self, subparsers): parser = self.add_subcommand(subparsers) - parser.prog = 'plainbox session' - parser.set_defaults(default_session_cmd='list') + parser.prog = "plainbox session" + parser.set_defaults(default_session_cmd="list") # Duplicate the default value of --only-ids This is only used when # we use the default command aka when 'plainbox session' runs. parser.set_defaults(only_ids=False) session_subparsers = parser.add_subparsers( - title=_('available session subcommands')) + title=_("available session subcommands") + ) list_parser = session_subparsers.add_parser( - 'list', help=_('list available sessions')) + "list", help=_("list available sessions") + ) list_parser.add_argument( - '--only-ids', help=_('print one id per line only'), - action='store_true', default=False) - list_parser.set_defaults(session_cmd='list') + "--only-ids", + help=_("print one id per line only"), + action="store_true", + default=False, + ) + list_parser.set_defaults(session_cmd="list") remove_parser = session_subparsers.add_parser( - 'remove', help=_('remove one more more sessions')) + "remove", help=_("remove one more more sessions") + ) remove_parser.add_argument( - 'session_id_list', metavar=_('SESSION-ID'), nargs="+", - help=_('Identifier of the session to remove')) - remove_parser.set_defaults(session_cmd='remove') + "session_id_list", + metavar=_("SESSION-ID"), + nargs="+", + help=_("Identifier of the session to remove"), + ) + remove_parser.set_defaults(session_cmd="remove") show_parser = session_subparsers.add_parser( - 'show', help=_('show a single session')) + "show", help=_("show a single session") + ) show_parser.add_argument( - 'session_id_list', metavar=_('SESSION-ID'), nargs="+", - help=_('Identifier of the session to show')) + "session_id_list", + metavar=_("SESSION-ID"), + nargs="+", + help=_("Identifier of the session to show"), + ) show_parser.add_argument( - '-r', '--resume', action='store_true', - help=_("resume the session (useful for debugging)")) + "-r", + "--resume", + action="store_true", + help=_("resume the session (useful for debugging)"), + ) show_parser.add_argument( - '-f', '--flag', action='append', metavar=_("FLAG"), - help=_("pass this resume flag to the session resume code")) - show_parser.set_defaults(session_cmd='show') + "-f", + "--flag", + action="append", + metavar=_("FLAG"), + help=_("pass this resume flag to the session resume code"), + ) + show_parser.set_defaults(session_cmd="show") archive_parser = session_subparsers.add_parser( - 'archive', help=_('archive a single session')) + "archive", help=_("archive a single session") + ) archive_parser.add_argument( - 'session_id', metavar=_('SESSION-ID'), - help=_('Identifier of the session to archive')) + "session_id", + metavar=_("SESSION-ID"), + help=_("Identifier of the session to archive"), + ) archive_parser.add_argument( - 'archive', metavar=_('ARCHIVE'), - help=_('Name of the archive to create')) - archive_parser.set_defaults(session_cmd='archive') + "archive", + metavar=_("ARCHIVE"), + help=_("Name of the archive to create"), + ) + archive_parser.set_defaults(session_cmd="archive") export_parser = session_subparsers.add_parser( - 'export', help=_('export a single session')) + "export", help=_("export a single session") + ) export_parser.add_argument( - 'session_id', metavar=_('SESSION-ID'), - help=_('Identifier of the session to export')) + "session_id", + metavar=_("SESSION-ID"), + help=_("Identifier of the session to export"), + ) export_parser.add_argument( - '--flag', action='append', metavar=_("FLAG"), - help=_("pass this resume flag to the session resume code")) - export_parser.set_defaults(session_cmd='export') + "--flag", + action="append", + metavar=_("FLAG"), + help=_("pass this resume flag to the session resume code"), + ) + export_parser.set_defaults(session_cmd="export") group = export_parser.add_argument_group(_("output options")) group.add_argument( - '-f', '--output-format', default='text', - metavar=_('FORMAT'), - help=_('save test results in the specified FORMAT' - ' (pass ? for a list of choices)')) + "-f", + "--output-format", + default="text", + metavar=_("FORMAT"), + help=_( + "save test results in the specified FORMAT" + " (pass ? for a list of choices)" + ), + ) group.add_argument( - '-p', '--output-options', default='', - metavar=_('OPTIONS'), - help=_('comma-separated list of options for the export mechanism' - ' (pass ? for a list of choices)')) + "-p", + "--output-options", + default="", + metavar=_("OPTIONS"), + help=_( + "comma-separated list of options for the export mechanism" + " (pass ? for a list of choices)" + ), + ) group.add_argument( - '-o', '--output-file', default='-', - metavar=_('FILE'), type=FileType("wb"), - help=_('save test results to the specified FILE' - ' (or to stdout if FILE is -)')) + "-o", + "--output-file", + default="-", + metavar=_("FILE"), + type=FileType("wb"), + help=_( + "save test results to the specified FILE" + " (or to stdout if FILE is -)" + ), + ) diff --git a/checkbox-ng/plainbox/impl/commands/dev.py b/checkbox-ng/plainbox/impl/commands/dev.py index c6e243165..f82e3de3c 100644 --- a/checkbox-ng/plainbox/impl/commands/dev.py +++ b/checkbox-ng/plainbox/impl/commands/dev.py @@ -48,8 +48,10 @@ def invoked(self, ns): def register_parser(self, subparsers): parser = subparsers.add_parser( - "dev", help=_("development commands"), + "dev", + help=_("development commands"), prog="plainbox dev", - usage=_("plainbox dev ...")) + usage=_("plainbox dev ..."), + ) subdev = parser.add_subparsers() ParseCommand().register_parser(subdev) diff --git a/checkbox-ng/plainbox/impl/commands/inv_parse.py b/checkbox-ng/plainbox/impl/commands/inv_parse.py index 288a968ea..c9da13321 100644 --- a/checkbox-ng/plainbox/impl/commands/inv_parse.py +++ b/checkbox-ng/plainbox/impl/commands/inv_parse.py @@ -30,7 +30,7 @@ class ParseInvocation: Invocation of the 'parse' command """ - def __init__(self, parser, encoding='UTF-8'): + def __init__(self, parser, encoding="UTF-8"): self.parser = parser self.encoding = encoding @@ -39,9 +39,11 @@ def run(self): # stdin unfortunately has when piped to. Using the embedded 'buffer' # attribute of sys.stdin we can construct a TextIOWrapper with # different, arbitrary encoding. - if (isinstance(sys.stdin, io.TextIOWrapper) - and sys.stdin.encoding != self.encoding): - with io.TextIOWrapper(sys.stdin.buffer, encoding='UTF-8') as stdin: + if ( + isinstance(sys.stdin, io.TextIOWrapper) + and sys.stdin.encoding != self.encoding + ): + with io.TextIOWrapper(sys.stdin.buffer, encoding="UTF-8") as stdin: text = self._do_read(stdin) else: text = self._do_read(sys.stdin) @@ -61,6 +63,8 @@ def _do_read(self, stream): try: return stream.read() except UnicodeEncodeError: - print(_("Unable to decode input stream, must be valid UTF-8"), - file=sys.stderr) + print( + _("Unable to decode input stream, must be valid UTF-8"), + file=sys.stderr, + ) return None diff --git a/checkbox-ng/plainbox/impl/commands/inv_session.py b/checkbox-ng/plainbox/impl/commands/inv_session.py index 12afa207f..24c37353e 100644 --- a/checkbox-ng/plainbox/impl/commands/inv_session.py +++ b/checkbox-ng/plainbox/impl/commands/inv_session.py @@ -53,16 +53,16 @@ def __init__(self, ns, provider_loader): self.provider_loader = provider_loader def run(self): - cmd = getattr(self.ns, 'session_cmd', self.ns.default_session_cmd) - if cmd == 'list': + cmd = getattr(self.ns, "session_cmd", self.ns.default_session_cmd) + if cmd == "list": self.list_sessions() - elif cmd == 'remove': + elif cmd == "remove": self.remove_session() - elif cmd == 'show': + elif cmd == "show": self.show_session() - elif cmd == 'archive': + elif cmd == "archive": self.archive_session() - elif cmd == 'export': + elif cmd == "export": self.export_session() def list_sessions(self): @@ -74,9 +74,14 @@ def list_sessions(self): data = storage.load_checkpoint() if len(data) > 0: metadata = SessionPeekHelper().peek(data) - print(_("session {0} app:{1}, flags:{2!r}, title:{3!r}") - .format(storage.id, metadata.app_id, - sorted(metadata.flags), metadata.title)) + print( + _("session {0} app:{1}, flags:{2!r}, title:{3!r}").format( + storage.id, + metadata.app_id, + sorted(metadata.flags), + metadata.title, + ) + ) else: print(_("session {0} (not saved yet)").format(storage.id)) if not self.ns.only_ids and storage is None: @@ -104,13 +109,20 @@ def show_session(self): continue metadata = SessionPeekHelper().peek(data) print(_("application ID: {0!r}").format(metadata.app_id)) - print(_("application-specific blob: {0}").format( - b64encode(metadata.app_blob).decode('ASCII') - if metadata.app_blob is not None else None)) + print( + _("application-specific blob: {0}").format( + b64encode(metadata.app_blob).decode("ASCII") + if metadata.app_blob is not None + else None + ) + ) print(_("session title: {0!r}").format(metadata.title)) print(_("session flags: {0!r}").format(sorted(metadata.flags))) - print(_("current job ID: {0!r}").format( - metadata.running_job_name)) + print( + _("current job ID: {0!r}").format( + metadata.running_job_name + ) + ) print(_("data size: {0}").format(len(data))) if self.ns.resume: print(_("Resuming session {0} ...").format(storage.id)) @@ -123,7 +135,8 @@ def show_session(self): def resume_session(self, storage): return SessionManager.load_session( - self._get_all_units(), storage, flags=self.ns.flag) + self._get_all_units(), storage, flags=self.ns.flag + ) def archive_session(self): session_id = self.ns.session_id @@ -133,16 +146,18 @@ def archive_session(self): else: print(_("Archiving session...")) archive = make_archive( - self.ns.archive, 'gztar', + self.ns.archive, + "gztar", os.path.dirname(storage.location), - os.path.basename(storage.location)) + os.path.basename(storage.location), + ) print(_("Created archive: {0}").format(archive)) def export_session(self): - if self.ns.output_format == _('?'): + if self.ns.output_format == _("?"): self._print_output_format_list() return 0 - elif self.ns.output_options == _('?'): + elif self.ns.output_options == _("?"): self._print_output_option_list() return 0 storage = self._lookup_storage(self.ns.session_id) @@ -151,7 +166,8 @@ def export_session(self): else: print(_("Exporting session...")) manager = SessionManager.load_session( - self._get_all_units(), storage, flags=self.ns.flag) + self._get_all_units(), storage, flags=self.ns.flag + ) exporter = self._create_exporter(manager) # Get a stream with exported session data. exported_stream = io.BytesIO() @@ -162,33 +178,43 @@ def export_session(self): # This requires a bit more finesse, as exporters output bytes # and stdout needs a string. translating_stream = ByteStringStreamTranslator( - self.ns.output_file, "utf-8") + self.ns.output_file, "utf-8" + ) copyfileobj(exported_stream, translating_stream) else: - print(_("Saving results to {}").format( - self.ns.output_file.name)) + print( + _("Saving results to {}").format(self.ns.output_file.name) + ) copyfileobj(exported_stream, self.ns.output_file) if self.ns.output_file is not sys.stdout: self.ns.output_file.close() def _get_all_units(self): return list( - itertools.chain(*[p.unit_list for p in self.provider_loader()])) + itertools.chain(*[p.unit_list for p in self.provider_loader()]) + ) def _print_output_format_list(self): - print(_("Available output formats: {}").format( - ', '.join(get_all_exporter_names()))) + print( + _("Available output formats: {}").format( + ", ".join(get_all_exporter_names()) + ) + ) def _print_output_option_list(self): print(_("Each format may support a different set of options")) with SessionManager.get_throwaway_manager() as manager: for name, exporter in manager.exporter_map.items(): - print("{}: {}".format( - name, ", ".join(exporter.exporter_cls.supported_option_list))) + print( + "{}: {}".format( + name, + ", ".join(exporter.exporter_cls.supported_option_list), + ) + ) def _create_exporter(self, manager): if self.ns.output_options: - option_list = self.ns.output_options.split(',') + option_list = self.ns.output_options.split(",") else: option_list = None return manager.create_exporter(self.ns.output_format, option_list) diff --git a/checkbox-ng/plainbox/impl/commands/parse.py b/checkbox-ng/plainbox/impl/commands/parse.py index c637ceef8..2cc2f027d 100644 --- a/checkbox-ng/plainbox/impl/commands/parse.py +++ b/checkbox-ng/plainbox/impl/commands/parse.py @@ -24,10 +24,12 @@ warnings.warn( "Use either plainbox.impl.commands.cmd_parse or .inv_parse instead", - PendingDeprecationWarning, stacklevel=2) + PendingDeprecationWarning, + stacklevel=2, +) -__all__ = ['ParseInvocation', 'ParseCommand'] +__all__ = ["ParseInvocation", "ParseCommand"] from plainbox.impl.commands.inv_parse import ParseInvocation from plainbox.impl.commands.cmd_parse import ParseCommand diff --git a/checkbox-ng/plainbox/impl/commands/session.py b/checkbox-ng/plainbox/impl/commands/session.py index 362375325..e4f560012 100644 --- a/checkbox-ng/plainbox/impl/commands/session.py +++ b/checkbox-ng/plainbox/impl/commands/session.py @@ -23,10 +23,12 @@ warnings.warn( "Use either plainbox.impl.commands.cmd_session or .inv_session instead", - PendingDeprecationWarning, stacklevel=2) + PendingDeprecationWarning, + stacklevel=2, +) -__all__ = ['SessionInvocation', 'SessionCommand'] +__all__ = ["SessionInvocation", "SessionCommand"] from plainbox.impl.commands.inv_session import SessionInvocation from plainbox.impl.commands.cmd_session import SessionCommand diff --git a/checkbox-ng/plainbox/impl/commands/test_deprecated.py b/checkbox-ng/plainbox/impl/commands/test_deprecated.py index c186c0789..8d4ed66a1 100644 --- a/checkbox-ng/plainbox/impl/commands/test_deprecated.py +++ b/checkbox-ng/plainbox/impl/commands/test_deprecated.py @@ -23,9 +23,11 @@ class DeprecatedTests(TestCase): @mock.patch("warnings.warn") def test_parse_warning(self, warn_mock): import plainbox.impl.commands.parse + self.assertTrue(warn_mock.called) @mock.patch("warnings.warn") def test_session_warning(self, warn_mock): import plainbox.impl.commands.session + self.assertTrue(warn_mock.called) diff --git a/checkbox-ng/plainbox/impl/commands/test_dev.py b/checkbox-ng/plainbox/impl/commands/test_dev.py index b8242f27e..14cd148dc 100644 --- a/checkbox-ng/plainbox/impl/commands/test_dev.py +++ b/checkbox-ng/plainbox/impl/commands/test_dev.py @@ -34,7 +34,7 @@ class TestDevCommand(TestCase): def setUp(self): - self.parser = argparse.ArgumentParser(prog='test') + self.parser = argparse.ArgumentParser(prog="test") self.subparsers = self.parser.add_subparsers() self.provider_loader = lambda: [mock.Mock()] self.config_loader = lambda: mock.Mock() @@ -47,16 +47,18 @@ def test_init(self): def test_register_parser(self): DevCommand(self.provider_loader, self.config_loader).register_parser( - self.subparsers) + self.subparsers + ) with TestIO() as io: self.parser.print_help() self.assertIn("development commands", io.stdout) with TestIO() as io: with self.assertRaises(SystemExit): - self.parser.parse_args(['dev', '--help']) + self.parser.parse_args(["dev", "--help"]) self.maxDiff = None self.assertEqual( - io.stdout, cleandoc( + io.stdout, + cleandoc( """ usage: plainbox dev ... @@ -66,5 +68,9 @@ def test_register_parser(self): {}: -h, --help show this help message and exit - """.format(optionals_section)) - + "\n") + """.format( + optionals_section + ) + ) + + "\n", + ) diff --git a/checkbox-ng/plainbox/impl/commands/test_inv_session.py b/checkbox-ng/plainbox/impl/commands/test_inv_session.py index a3b8804ac..a5575af11 100644 --- a/checkbox-ng/plainbox/impl/commands/test_inv_session.py +++ b/checkbox-ng/plainbox/impl/commands/test_inv_session.py @@ -25,9 +25,7 @@ class SessionInvocationTests(TestCase): @mock.patch( "plainbox.impl.commands.inv_session.SessionInvocation._lookup_storage" ) - @mock.patch( - "builtins.print" - ) + @mock.patch("builtins.print") def test_register_parser_none(self, print_mock, lookup_mock): lookup_mock.return_value = None ns = mock.MagicMock() diff --git a/checkbox-ng/plainbox/impl/commands/test_parse.py b/checkbox-ng/plainbox/impl/commands/test_parse.py index 9dd382ba7..224ce9019 100644 --- a/checkbox-ng/plainbox/impl/commands/test_parse.py +++ b/checkbox-ng/plainbox/impl/commands/test_parse.py @@ -57,13 +57,15 @@ def test_init(self): -h, --help show this help message and exit Example: LANG=C pactl list | plainbox dev parse pactl-list -""".format(optionals_section) +""".format( + optionals_section + ) maxDiff = None def test_register_parser(self): # Create an argument parser - parser = argparse.ArgumentParser(prog='test') + parser = argparse.ArgumentParser(prog="test") # Add subparsers to it subparsers = parser.add_subparsers() # Register the ParseCommand into subparsers @@ -79,9 +81,9 @@ def test_register_parser(self): # With a trap for SystemExit exception with self.assertRaises(SystemExit): # Run the 'parse --help' command - parser.parse_args(['parse', '--help']) + parser.parse_args(["parse", "--help"]) # Ensure that a detailed help message was printed - self.assertEqual(io.stdout, cleandoc(self._help) + '\n') + self.assertEqual(io.stdout, cleandoc(self._help) + "\n") @mock.patch("plainbox.impl.commands.inv_parse.ParseInvocation") def test_invoked(self, patched_ParseInvocation): @@ -93,7 +95,7 @@ def test_invoked(self, patched_ParseInvocation): # With temporary override to use the fake parser with all_parsers.fake_plugins([mock_parser]): # Set the name of the expected parser to 'foo' - self.ns.parser_name = 'foo' + self.ns.parser_name = "foo" # And invoke the ParseCommand retval = ParseCommand().invoked(self.ns) # Ensure that ParseInvocation was called with the fake parser @@ -102,7 +104,8 @@ def test_invoked(self, patched_ParseInvocation): # was returned by ParseInvocation.run() self.assertEqual( retval, - patched_ParseInvocation(self.ns.parser_name).run.return_value) + patched_ParseInvocation(self.ns.parser_name).run.return_value, + ) def test_invoked_question_mark(self): # Make a fake ParserPlugIn @@ -114,17 +117,21 @@ def test_invoked_question_mark(self): # With temporary override to use the fake parser with all_parsers.fake_plugins([mock_parser]): # Set the name of the expected parser to '?' - self.ns.parser_name = '?' + self.ns.parser_name = "?" # With IO capture helper with TestIO() as io: # And invoke the ParseCommand retval = ParseCommand().invoked(self.ns) # Ensure that a list of parsers was printed self.assertEqual( - io.stdout, cleandoc( + io.stdout, + cleandoc( """ The following parsers are available: foo: summary of foo - """) + '\n') + """ + ) + + "\n", + ) # Ensure that the return code was 0 self.assertEqual(retval, 0) diff --git a/checkbox-ng/plainbox/impl/config.py b/checkbox-ng/plainbox/impl/config.py index 58c830e00..84088ca2b 100644 --- a/checkbox-ng/plainbox/impl/config.py +++ b/checkbox-ng/plainbox/impl/config.py @@ -41,9 +41,7 @@ class Configuration: and many others. Look at CONFIG_SPEC for details. """ - DEPRECATED_SECTION_NAMES = { - "daemon": "agent" - } + DEPRECATED_SECTION_NAMES = {"daemon": "agent"} def __init__(self, source=None): """Create a new configuration object filled with default values.""" @@ -263,7 +261,8 @@ def from_ini_file(cls, ini_file, origin): current_name = cls.DEPRECATED_SECTION_NAMES[sect_name] logger.warning( "Config: %s section name is deprecated. Use %s instead.", - sect_name, current_name + sect_name, + current_name, ) sect_name = current_name if ":" in sect_name: diff --git a/checkbox-ng/plainbox/impl/decorators.py b/checkbox-ng/plainbox/impl/decorators.py index 18b7546e2..0ec495d95 100644 --- a/checkbox-ng/plainbox/impl/decorators.py +++ b/checkbox-ng/plainbox/impl/decorators.py @@ -22,22 +22,25 @@ import functools import logging -__all__ = ['raises'] +__all__ = ["raises"] _bug_logger = logging.getLogger("plainbox.bug") def instance_method_lru_cache(*cache_args, **cache_kwargs): - ''' + """ Just like functools.lru_cache, but a new cache is created for each instance of the class that owns the method this is applied to. See https://gist.github.com/z0u/9df24dda2b1fe0613a85e7349d5f7d62 - ''' + """ + def cache_decorator(func): @functools.wraps(func) def cache_factory(self, *args, **kwargs): # Wrap the function in a cache by calling the decorator - instance_cache = functools.lru_cache(*cache_args, **cache_kwargs)(func) + instance_cache = functools.lru_cache(*cache_args, **cache_kwargs)( + func + ) # Bind the decorated function to the instance to make it a method instance_cache = instance_cache.__get__(self, self.__class__) setattr(self, func.__name__, instance_cache) @@ -45,7 +48,9 @@ def cache_factory(self, *args, **kwargs): # call will go directly to the instance cache and not via this # decorator. return instance_cache(*args, **kwargs) + return cache_factory + return cache_decorator @@ -55,6 +60,7 @@ class cached_property(object): property cached on the instance. See https://goo.gl/QgJYks (django cached_property) """ + def __init__(self, func): self.func = func @@ -92,7 +98,8 @@ def __str__(self): self.func, self.func.__code__.co_filename, self.func.__code__.co_firstlineno, - self.exc_cls.__name__) + self.exc_cls.__name__, + ) def raises(*exc_cls_list: BaseException): @@ -119,14 +126,16 @@ def raises(*exc_cls_list: BaseException): valued) which contains the list of exceptions that may be raised. """ for exc_cls in exc_cls_list: - if not isinstance(exc_cls, type) or not issubclass(exc_cls, BaseException): + if not isinstance(exc_cls, type) or not issubclass( + exc_cls, BaseException + ): raise TypeError("All arguments must be exceptions") def decorator(func): # Enforce documentation of all the exceptions if func.__doc__ is not None: for exc_cls in exc_cls_list: - if ':raises {}:'.format(exc_cls.__name__) not in func.__doc__: + if ":raises {}:".format(exc_cls.__name__) not in func.__doc__: raise UndocumentedException(func, exc_cls) # Wrap in detector function @@ -138,13 +147,18 @@ def wrapper(*args, **kwargs): if not isinstance(exc, exc_cls_list): _bug_logger.error( "Undeclared exception %s raised from %s", - exc.__class__.__name__, func.__name__) + exc.__class__.__name__, + func.__name__, + ) raise exc + # Annotate the function and the wrapper - wrapper.__annotations__['raise'] = exc_cls_list - func.__annotations__['raise'] = exc_cls_list + wrapper.__annotations__["raise"] = exc_cls_list + func.__annotations__["raise"] = exc_cls_list return wrapper + return decorator + # Annotate thyself raises = raises(TypeError)(raises) diff --git a/checkbox-ng/plainbox/impl/depmgr.py b/checkbox-ng/plainbox/impl/depmgr.py index d0e9c7c07..828a716e3 100644 --- a/checkbox-ng/plainbox/impl/depmgr.py +++ b/checkbox-ng/plainbox/impl/depmgr.py @@ -40,12 +40,11 @@ class DependencyError(Exception, metaclass=ABCMeta): - - """ Exception raised when a dependency error is detected. """ + """Exception raised when a dependency error is detected.""" @abstractproperty def affected_job(self): - """ job that is affected by the dependency error. """ + """job that is affected by the dependency error.""" @abstractproperty def affecting_job(self): @@ -61,7 +60,6 @@ def affecting_job(self): class DependencyUnknownError(DependencyError): - """ Exception raised when an unknown job is mentioned. @@ -72,7 +70,7 @@ class DependencyUnknownError(DependencyError): """ def __init__(self, job): - """ Initialize a new DependencyUnknownError with a given job. """ + """Initialize a new DependencyUnknownError with a given job.""" self.job = job @property @@ -93,27 +91,26 @@ def affecting_job(self): """ def __str__(self): - """ Get a printable description of an error. """ + """Get a printable description of an error.""" return _("unknown job referenced: {!a}").format(self.job.id) def __repr__(self): - """ Get a debugging representation of an error. """ + """Get a debugging representation of an error.""" return "<{} job:{!r}>".format(self.__class__.__name__, self.job) def __eq__(self, other): - """ Check if one error is equal to another. """ + """Check if one error is equal to another.""" if not isinstance(other, DependencyUnknownError): return NotImplemented return self.job == other.job def __hash__(self): - """ Calculate the hash of an error. """ + """Calculate the hash of an error.""" return hash((self.job,)) class DependencyCycleError(DependencyError): - - """ Exception raised when a cyclic dependency is detected. """ + """Exception raised when a cyclic dependency is detected.""" def __init__(self, job_list): """ @@ -149,26 +146,27 @@ def affecting_job(self): return self.affected_job def __str__(self): - """ Get a printable description of an error. """ + """Get a printable description of an error.""" return _("dependency cycle detected: {}").format( - " -> ".join([job.id for job in self.job_list])) + " -> ".join([job.id for job in self.job_list]) + ) def __repr__(self): - """ Get a debugging representation of an error. """ + """Get a debugging representation of an error.""" return "<{} job_list:{!r}>".format( - self.__class__.__name__, self.job_list) + self.__class__.__name__, self.job_list + ) class DependencyMissingError(DependencyError): - - """ Exception raised when a job has an unsatisfied dependency. """ + """Exception raised when a job has an unsatisfied dependency.""" DEP_TYPE_RESOURCE = "resource" DEP_TYPE_DIRECT = "direct" DEP_TYPE_ORDERING = "ordering" def __init__(self, job, missing_job_id, dep_type): - """ Initialize a new error with given data. """ + """Initialize a new error with given data.""" self.job = job self.missing_job_id = missing_job_id self.dep_type = dep_type @@ -192,35 +190,40 @@ def affecting_job(self): """ def __str__(self): - """ Get a printable description of an error. """ + """Get a printable description of an error.""" return _("missing dependency: {!r} ({})").format( - self.missing_job_id, self.dep_type) + self.missing_job_id, self.dep_type + ) def __repr__(self): - """ Get a debugging representation of an error. """ + """Get a debugging representation of an error.""" return "<{} job:{!r} missing_job_id:{!r} dep_type:{!r}>".format( self.__class__.__name__, - self.job, self.missing_job_id, self.dep_type) + self.job, + self.missing_job_id, + self.dep_type, + ) def __eq__(self, other): - """ Check if one error is equal to another. """ + """Check if one error is equal to another.""" if not isinstance(other, DependencyMissingError): return NotImplemented - return (self.job == other.job - and self.missing_job_id == other.missing_job_id - and self.dep_type == other.dep_type) + return ( + self.job == other.job + and self.missing_job_id == other.missing_job_id + and self.dep_type == other.dep_type + ) def __hash__(self): - """ Calculate the hash of an error. """ + """Calculate the hash of an error.""" return hash((self.job, self.missing_job_id, self.dep_type)) class DependencyDuplicateError(DependencyError): - - """ Exception raised when two jobs have the same id. """ + """Exception raised when two jobs have the same id.""" def __init__(self, job, duplicate_job): - """ Initialize a new error with given data. """ + """Initialize a new error with given data.""" assert job.id == duplicate_job.id self.job = job self.duplicate_job = duplicate_job @@ -245,17 +248,17 @@ def affecting_job(self): return self.duplicate_job def __str__(self): - """ Get a printable description of an error. """ + """Get a printable description of an error.""" return _("duplicate job id: {!r}").format(self.affected_job.id) def __repr__(self): - """ Get a debugging representation of an error. """ + """Get a debugging representation of an error.""" return "<{} job:{!r} duplicate_job:{!r}>".format( - self.__class__.__name__, self.job, self.duplicate_job) + self.__class__.__name__, self.job, self.duplicate_job + ) class Color(enum.Enum): - """ Three classic colors for recursive graph visitor. @@ -268,13 +271,12 @@ class Color(enum.Enum): For nodes that have been visited and are complete. """ - WHITE = 'white' - GRAY = 'gray' - BLACK = 'black' + WHITE = "white" + GRAY = "gray" + BLACK = "black" class DependencySolver: - """ Dependency solver for Jobs. @@ -376,8 +378,9 @@ def _visit(self, job, visit_list, trail=None): try: next_job = self._job_map[job_id] except KeyError: - logger.debug(_("Found missing dependency: %r from %r"), - job_id, job) + logger.debug( + _("Found missing dependency: %r from %r"), job_id, job + ) raise DependencyMissingError(job, job_id, dep_type) else: # For each dependency that we visit let's reuse the trail @@ -397,7 +400,7 @@ def _visit(self, job, visit_list, trail=None): # so we've found a dependency loop. We need to cut the initial # part of the trail so that we only report the part that actually # forms a loop - trail = trail[trail.index(job):] + trail = trail[trail.index(job) :] logger.debug(_("Found dependency cycle: %r"), trail) raise DependencyCycleError(trail) else: diff --git a/checkbox-ng/plainbox/impl/developer.py b/checkbox-ng/plainbox/impl/developer.py index a04c7f390..635615f2f 100644 --- a/checkbox-ng/plainbox/impl/developer.py +++ b/checkbox-ng/plainbox/impl/developer.py @@ -22,18 +22,16 @@ import logging import warnings -__all__ = ('UsageExpectation',) +__all__ = ("UsageExpectation",) _logger = logging.getLogger("plainbox.developer") class OffByOneBackWarning(UserWarning): - """Warning on incorrect use of UsageExpectations(self).enforce(back=2).""" class DeveloperError(Exception): - """ Exception raised when program flow is incorrect. @@ -68,7 +66,6 @@ class DeveloperError(Exception): class UnexpectedMethodCall(DeveloperError): - """ Developer error reported when an unexpected method call is made. @@ -103,14 +100,16 @@ def __str__(self): cls_module=self.cls.__module__, cls_name=self.cls.__name__, fn_name=self.fn_name, - allowed_calls='\n'.join( - ' - call {}.{}() to {}.'.format( - self.cls.__name__, allowed_fn_name, why) - for allowed_fn_name, why in self.allowed_pairs)) + allowed_calls="\n".join( + " - call {}.{}() to {}.".format( + self.cls.__name__, allowed_fn_name, why + ) + for allowed_fn_name, why in self.allowed_pairs + ), + ) class UsageExpectation: - """ Class representing API usage expectation at any given time. @@ -182,8 +181,11 @@ def enforce(self, back=1): # optimized values (for computing what is really allowed) must be # obtained each time we are about to check, in enforce() allowed_code = frozenset( - func.__wrapped__.__code__ - if hasattr(func, '__wrapped__') else func.__code__ + ( + func.__wrapped__.__code__ + if hasattr(func, "__wrapped__") + else func.__code__ + ) for func in self.allowed_calls ) caller_frame = inspect.stack(0)[back][0] @@ -192,32 +194,39 @@ def enforce(self, back=1): else: alt_caller_frame = None _logger.debug("Caller code: %r", caller_frame.f_code) - _logger.debug("Alternate code: %r", - alt_caller_frame.f_code if alt_caller_frame else None) + _logger.debug( + "Alternate code: %r", + alt_caller_frame.f_code if alt_caller_frame else None, + ) _logger.debug("Allowed code: %r", allowed_code) try: if caller_frame.f_code in allowed_code: return # This can be removed later, it allows the caller to make an # off-by-one mistake and go away with it. - if (alt_caller_frame is not None and - alt_caller_frame.f_code in allowed_code): + if ( + alt_caller_frame is not None + and alt_caller_frame.f_code in allowed_code + ): warnings.warn( "Please back={}. Properly constructed decorators are" " automatically handled and do not require the use of the" - " back argument.".format(back - 1), OffByOneBackWarning, - back) + " back argument.".format(back - 1), + OffByOneBackWarning, + back, + ) return fn_name = caller_frame.f_code.co_name allowed_undecorated_calls = { - func.__wrapped__ if hasattr(func, '__wrapped__') else func: msg + func.__wrapped__ if hasattr(func, "__wrapped__") else func: msg for func, msg in self.allowed_calls.items() } allowed_pairs = tuple( (fn.__code__.co_name, why) for fn, why in sorted( allowed_undecorated_calls.items(), - key=lambda fn_why: fn_why[0].__code__.co_name) + key=lambda fn_why: fn_why[0].__code__.co_name, + ) ) raise UnexpectedMethodCall(self.cls, fn_name, allowed_pairs) finally: diff --git a/checkbox-ng/plainbox/impl/execution.py b/checkbox-ng/plainbox/impl/execution.py index cecb41fea..f0619fd94 100644 --- a/checkbox-ng/plainbox/impl/execution.py +++ b/checkbox-ng/plainbox/impl/execution.py @@ -60,12 +60,19 @@ class UnifiedRunner(IJobRunner): environment for job's command can run in. """ - def __init__(self, session_id, provider_list, jobs_io_log_dir, - command_io_delegate=None, dry_run=False, - execution_ctrl_list=None, stdin=False, - normal_user_provider=lambda: None, - password_provider=sudo_password_provider.get_sudo_password, - extra_env=None): + def __init__( + self, + session_id, + provider_list, + jobs_io_log_dir, + command_io_delegate=None, + dry_run=False, + execution_ctrl_list=None, + stdin=False, + normal_user_provider=lambda: None, + password_provider=sudo_password_provider.get_sudo_password, + extra_env=None, + ): self._session_id = session_id self._provider_list = provider_list if execution_ctrl_list is not None: @@ -85,31 +92,35 @@ def __init__(self, session_id, provider_list, jobs_io_log_dir, def run_job(self, job, job_state, environ=None, ui=None): logger.info(_("Running %r"), job) if job.plugin not in supported_plugins: - print(Colorizer().RED("Unsupported plugin type: {}".format( - job.plugin))) + print( + Colorizer().RED( + "Unsupported plugin type: {}".format(job.plugin) + ) + ) return JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, - comments=_("Unsupported plugin type: {}".format(job.plugin)) + comments=_("Unsupported plugin type: {}".format(job.plugin)), ).get_result() # resource and attachment jobs are always run (even in dry runs) - if self._dry_run and job.plugin not in ('resource', 'attachment'): + if self._dry_run and job.plugin not in ("resource", "attachment"): return JobResultBuilder( outcome=IJobResult.OUTCOME_SKIP, - comments=_("Job skipped in dry-run mode") + comments=_("Job skipped in dry-run mode"), ).get_result() self._job_runner_ui_delegate.ui = ui # for cached resource jobs we get the result using cache # if it's not in the cache, ordinary "_run_command" will be run - if job.plugin == 'resource' and 'cachable' in job.get_flag_set(): + if job.plugin == "resource" and "cachable" in job.get_flag_set(): from_cache, result = self._resource_cache.get( - job.checksum, lambda: self._run_command( - job, environ).get_result()) + job.checksum, + lambda: self._run_command(job, environ).get_result(), + ) if from_cache: print(Colorizer().header(_("Using cached data!"))) jrud = self._job_runner_ui_delegate - jrud.on_begin('', dict()) + jrud.on_begin("", dict()) for io_log_entry in result.io_log: jrud.on_chunk(io_log_entry.stream_name, io_log_entry.data) jrud.on_end(result.return_code) @@ -117,23 +128,24 @@ def run_job(self, job, job_state, environ=None, ui=None): # manual jobs don't require running anything so we just return # the 'undecided' outcome - if job.plugin == 'manual': + if job.plugin == "manual": return JobResultBuilder( - outcome=IJobResult.OUTCOME_UNDECIDED).get_result() + outcome=IJobResult.OUTCOME_UNDECIDED + ).get_result() # all other kinds of jobs at this point need to run their command if not job.command: print(Colorizer().RED("No command to run!")) return JobResultBuilder( outcome=IJobResult.OUTCOME_FAIL, - comments=_("No command to run!") + comments=_("No command to run!"), ).get_result() result_builder = self._run_command(job, environ) # for user-interact-verify and user-verify jobs the operator chooses # the final outcome, so we need to reset the outcome to undecided # (from what command's return code would have set) - if job.plugin in ('user-interact-verify', 'user-verify'): + if job.plugin in ("user-interact-verify", "user-verify"): result_builder.outcome = IJobResult.OUTCOME_UNDECIDED # by this point the result_builder should have all the info needed @@ -150,18 +162,27 @@ def _run_command(self, job, environ): slug = slugify(job.id) output_writer = CommandOutputWriter( stdout_path=os.path.join( - self._jobs_io_log_dir, "{}.stdout".format(slug)), + self._jobs_io_log_dir, "{}.stdout".format(slug) + ), stderr_path=os.path.join( - self._jobs_io_log_dir, "{}.stderr".format(slug))) + self._jobs_io_log_dir, "{}.stderr".format(slug) + ), + ) io_log_gen = IOLogRecordGenerator() log = os.path.join(self._jobs_io_log_dir, "{}.record.gz".format(slug)) - with gzip.open(log, mode='wb') as gzip_stream, io.TextIOWrapper( - gzip_stream, encoding='UTF-8') as record_stream: + with gzip.open(log, mode="wb") as gzip_stream, io.TextIOWrapper( + gzip_stream, encoding="UTF-8" + ) as record_stream: writer = IOLogRecordWriter(record_stream) io_log_gen.on_new_record.connect(writer.write_record) - delegate = extcmd.Chain([ - self._job_runner_ui_delegate, io_log_gen, - self._command_io_delegate, output_writer]) + delegate = extcmd.Chain( + [ + self._job_runner_ui_delegate, + io_log_gen, + self._command_io_delegate, + output_writer, + ] + ) ecmd = extcmd.ExternalCommandWithDelegate(delegate) return_code = self.execute_job(job, environ, ecmd, self._stdin) io_log_gen.on_new_record.disconnect(writer.write_record) @@ -175,7 +196,8 @@ def _run_command(self, job, environ): outcome=outcome, return_code=return_code, io_log_filename=log, - execution_duration=time.time() - start_time) + execution_duration=time.time() - start_time, + ) def execute_job(self, job, environ, extcmd_popen, stdin=None): """Run the 'binary' associated with the job.""" @@ -189,9 +211,9 @@ def call(extcmd_popen, *args, **kwargs): # Notify that the process is about to start extcmd_popen._delegate.on_begin(args, kwargs) # Setup stdout/stderr redirection - kwargs['stdout'] = subprocess.PIPE - kwargs['stderr'] = subprocess.PIPE - kwargs['start_new_session'] = True + kwargs["stdout"] = subprocess.PIPE + kwargs["stderr"] = subprocess.PIPE + kwargs["start_new_session"] = True # Prepare stdio supply in_r, in_w = os.pipe() # first let's punch the password in @@ -201,7 +223,7 @@ def call(extcmd_popen, *args, **kwargs): if target_user and self._password_provider: password = self._password_provider() if password: - os.write(in_w, password + b'\n') + os.write(in_w, password + b"\n") def stdin_forwarder(stdin): """Forward data from one pipe to the other.""" @@ -211,7 +233,7 @@ def stdin_forwarder(stdin): while is_alive: if stdin in select.select([stdin], [], [], 0)[0]: buf = stdin.readline() - if buf == '': + if buf == "": break os.write(in_w, buf.encode(stdin.encoding)) else: @@ -219,10 +241,12 @@ def stdin_forwarder(stdin): except BrokenPipeError: pass os.close(in_w) + forwarder_thread = threading.Thread( - target=stdin_forwarder, args=(stdin,)) + target=stdin_forwarder, args=(stdin,) + ) forwarder_thread.start() - kwargs['stdin'] = in_r + kwargs["stdin"] = in_r # Start the process proc = extcmd_popen._popen(*args, **kwargs) @@ -230,9 +254,11 @@ def stdin_forwarder(stdin): # Setup all worker threads. By now the pipes have been created and # proc.stdout/proc.stderr point to open pipe objects. stdout_reader = threading.Thread( - target=extcmd_popen._read_stream, args=(proc.stdout, "stdout")) + target=extcmd_popen._read_stream, args=(proc.stdout, "stdout") + ) stderr_reader = threading.Thread( - target=extcmd_popen._read_stream, args=(proc.stderr, "stderr")) + target=extcmd_popen._read_stream, args=(proc.stderr, "stderr") + ) queue_worker = threading.Thread(target=extcmd_popen._drain_queue) # Start all workers queue_worker.start() @@ -246,6 +272,7 @@ def stdin_forwarder(stdin): except KeyboardInterrupt: is_alive = False import signal + self.send_signal(signal.SIGKILL, target_user) # And send a notification about this extcmd_popen._delegate.on_interrupt() @@ -265,33 +292,48 @@ def stdin_forwarder(stdin): # Notify that the process has finished extcmd_popen._delegate.on_end(proc.returncode) return proc.returncode + # Setup the executable nest directory with self.configured_filesystem(job) as nest_dir: # Get the command and the environment. # of this execution controller - cmd = get_execution_command(job, environ, self._session_id, - nest_dir, target_user, self._extra_env) - env = get_execution_environment(job, environ, self._session_id, - nest_dir) + cmd = get_execution_command( + job, + environ, + self._session_id, + nest_dir, + target_user, + self._extra_env, + ) + env = get_execution_environment( + job, environ, self._session_id, nest_dir + ) if self._user_provider(): - env['NORMAL_USER'] = self._user_provider() + env["NORMAL_USER"] = self._user_provider() # Always set SYSTEMD_IGNORE_CHROOT # See https://bugs.launchpad.net/snapd/+bug/2003955 env["SYSTEMD_IGNORE_CHROOT"] = "1" # run the command - logger.debug(_("job[%(ID)s] executing %(CMD)r with env %(ENV)r"), - {"ID": job.id, "CMD": cmd, - "ENV": env}) - if 'preserve-cwd' in job.get_flag_set() or os.getenv("SNAP"): + logger.debug( + _("job[%(ID)s] executing %(CMD)r with env %(ENV)r"), + {"ID": job.id, "CMD": cmd, "ENV": env}, + ) + if "preserve-cwd" in job.get_flag_set() or os.getenv("SNAP"): return_code = call( - extcmd_popen, cmd, stdin=subprocess.PIPE, env=env) + extcmd_popen, cmd, stdin=subprocess.PIPE, env=env + ) else: with self.temporary_cwd(job) as cwd_dir: return_code = call( - extcmd_popen, cmd, stdin=subprocess.PIPE, env=env, - cwd=cwd_dir) - if 'noreturn' in job.get_flag_set(): + extcmd_popen, + cmd, + stdin=subprocess.PIPE, + env=env, + cwd=cwd_dir, + ) + if "noreturn" in job.get_flag_set(): import signal + signal.pause() return return_code @@ -306,12 +348,13 @@ def configured_filesystem(self, job): Pathname of the executable symlink nest directory. """ # Create a nest for all the private executables needed for execution - prefix = 'nest-' - suffix = '.{}'.format(job.checksum) + prefix = "nest-" + suffix = ".{}".format(job.checksum) with tempfile.TemporaryDirectory(suffix, prefix) as nest_dir: os.chmod(nest_dir, 0o777) logger.debug(_("Symlink nest for executables: %s"), nest_dir) from plainbox.impl.ctrl import SymLinkNest + nest = SymLinkNest(nest_dir) # Add all providers sharing namespace with the current job to PATH for provider in self._provider_list: @@ -331,22 +374,25 @@ def temporary_cwd(self, job): Pathname of the new temporary directory """ # Create a nest for all the private executables needed for execution - prefix = 'cwd-' - suffix = '.{}'.format(job.checksum) + prefix = "cwd-" + suffix = ".{}".format(job.checksum) try: with tempfile.TemporaryDirectory(suffix, prefix) as cwd_dir: logger.debug( - _("Job temporary current working directory: %s"), cwd_dir) + _("Job temporary current working directory: %s"), cwd_dir + ) try: yield cwd_dir finally: - if 'has-leftovers' not in job.get_flag_set(): + if "has-leftovers" not in job.get_flag_set(): self._check_leftovers(cwd_dir, job) except PermissionError as exc: logger.warning( _("There was a problem with temporary cwd %s, %s"), - cwd_dir, exc) + cwd_dir, + exc, + ) def _check_leftovers(self, cwd_dir, job): leftovers = [] @@ -354,23 +400,31 @@ def _check_leftovers(self, cwd_dir, job): if dirpath != cwd_dir: leftovers.append(dirpath) leftovers.extend( - os.path.join(dirpath, filename) - for filename in filenames) + os.path.join(dirpath, filename) for filename in filenames + ) if leftovers: logger.warning( - _("Job {0} created leftover filesystem artefacts" - " in its working directory").format(job.id)) + _( + "Job {0} created leftover filesystem artefacts" + " in its working directory" + ).format(job.id) + ) for item in leftovers: - logger.warning(_( - "Leftover file/directory: %r"), - os.path.relpath(item, cwd_dir)) + logger.warning( + _("Leftover file/directory: %r"), + os.path.relpath(item, cwd_dir), + ) logger.warning( - _("Please store desired files in $PLAINBOX_SESSION_SHARE" - "and use regular temporary files for everything else")) + _( + "Please store desired files in $PLAINBOX_SESSION_SHARE" + "and use regular temporary files for everything else" + ) + ) def get_record_path_for_job(self, job): - return os.path.join(self._jobs_io_log_dir, - "{}.record.gz".format(slugify(job.id))) + return os.path.join( + self._jobs_io_log_dir, "{}.record.gz".format(slugify(job.id)) + ) def send_signal(self, signal, target_user): if not self._running_jobs_pid: @@ -383,10 +437,20 @@ def send_signal(self, signal, target_user): else: # process used sudo, so sudo is needed to kill it in_r, in_w = os.pipe() - os.write(in_w, self._password_provider() + b'\n') - cmd = ['sudo', '--prompt', '', '--reset-timestamp', '--stdin', - '--user', 'root', 'kill', '-s', str(signal), - '-{}'.format(self._running_jobs_pid)] + os.write(in_w, self._password_provider() + b"\n") + cmd = [ + "sudo", + "--prompt", + "", + "--reset-timestamp", + "--stdin", + "--user", + "root", + "kill", + "-s", + str(signal), + "-{}".format(self._running_jobs_pid), + ] try: subprocess.check_call(cmd, stdin=in_r) except subprocess.CalledProcessError: @@ -406,16 +470,18 @@ def run_job(self, job, job_state, environ=None, ui=None): Exception: 'graphics_card' resource job creates two objects to simulate hybrid graphics. """ - if job.plugin != 'resource': + if job.plugin != "resource": return super().run_job(job, job_state, environ, ui) builder = JobResultBuilder() - if job.partial_id == 'graphics_card': - builder.io_log = [(0, 'stdout', b'a: b\n'), - (1, 'stdout', b'\n'), - (2, 'stdout', b'a: c\n')] + if job.partial_id == "graphics_card": + builder.io_log = [ + (0, "stdout", b"a: b\n"), + (1, "stdout", b"\n"), + (2, "stdout", b"a: c\n"), + ] else: - builder.io_log = [(0, 'stdout', b'a: b\n')] - builder.outcome = 'pass' + builder.io_log = [(0, "stdout", b"a: b\n")] + builder.outcome = "pass" builder.return_code = 0 return builder.get_result() @@ -444,47 +510,58 @@ def get_execution_environment(job, environ, session_id, nest_dir): """ # Get a proper environment env = dict(os.environ) - if 'reset-locale' in job.get_flag_set(): + if "reset-locale" in job.get_flag_set(): # Use non-internationalized environment - env['LANG'] = 'C.UTF-8' - if 'LANGUAGE' in env: - del env['LANGUAGE'] + env["LANG"] = "C.UTF-8" + if "LANGUAGE" in env: + del env["LANGUAGE"] for name in list(env.keys()): if name.startswith("LC_"): del env[name] else: # Set the per-provider gettext domain and locale directory if job.provider.gettext_domain is not None: - env['TEXTDOMAIN'] = env['PLAINBOX_PROVIDER_GETTEXT_DOMAIN'] = \ + env["TEXTDOMAIN"] = env["PLAINBOX_PROVIDER_GETTEXT_DOMAIN"] = ( job.provider.gettext_domain + ) if job.provider.locale_dir is not None: - env['TEXTDOMAINDIR'] = env['PLAINBOX_PROVIDER_LOCALE_DIR'] = \ + env["TEXTDOMAINDIR"] = env["PLAINBOX_PROVIDER_LOCALE_DIR"] = ( job.provider.locale_dir - if (os.getenv("SNAP") or os.getenv("SNAP_APP_PATH")): - copy_vars = ['PYTHONHOME', 'PYTHONUSERBASE', 'LD_LIBRARY_PATH', - 'GI_TYPELIB_PATH', 'PERL5LIB'] + ) + if os.getenv("SNAP") or os.getenv("SNAP_APP_PATH"): + copy_vars = [ + "PYTHONHOME", + "PYTHONUSERBASE", + "LD_LIBRARY_PATH", + "GI_TYPELIB_PATH", + "PERL5LIB", + ] for key, value in env.items(): - if key in copy_vars or key.startswith('SNAP'): + if key in copy_vars or key.startswith("SNAP"): env[key] = value # Use PATH that can lookup checkbox scripts if job.provider.extra_PYTHONPATH: - env['PYTHONPATH'] = os.pathsep.join( - [job.provider.extra_PYTHONPATH] + env.get( - "PYTHONPATH", "").split(os.pathsep)) + env["PYTHONPATH"] = os.pathsep.join( + [job.provider.extra_PYTHONPATH] + + env.get("PYTHONPATH", "").split(os.pathsep) + ) # Inject nest_dir into PATH - env['PATH'] = os.pathsep.join( - [nest_dir] + env.get("PATH", "").split(os.pathsep)) + env["PATH"] = os.pathsep.join( + [nest_dir] + env.get("PATH", "").split(os.pathsep) + ) # Add per-session shared state directory - env['PLAINBOX_SESSION_SHARE'] = WellKnownDirsHelper.session_share( - session_id) + env["PLAINBOX_SESSION_SHARE"] = WellKnownDirsHelper.session_share( + session_id + ) def set_if_not_none(envvar, source): """Update env if the source variable is not None""" if source is not None: env[envvar] = source - set_if_not_none('PLAINBOX_PROVIDER_DATA', job.provider.data_dir) - set_if_not_none('PLAINBOX_PROVIDER_UNITS', job.provider.units_dir) - set_if_not_none('CHECKBOX_SHARE', job.provider.CHECKBOX_SHARE) + + set_if_not_none("PLAINBOX_PROVIDER_DATA", job.provider.data_dir) + set_if_not_none("PLAINBOX_PROVIDER_UNITS", job.provider.units_dir) + set_if_not_none("CHECKBOX_SHARE", job.provider.CHECKBOX_SHARE) # Inject additional variables that are requested in the config if environ is not None: for env_var in environ: @@ -499,8 +576,9 @@ def set_if_not_none(envvar, source): return env -def get_differential_execution_environment(job, environ, session_id, nest_dir, - extra_env=None): +def get_differential_execution_environment( + job, environ, session_id, nest_dir, extra_env=None +): """ Get the environment required to execute the specified job: @@ -534,28 +612,36 @@ def get_differential_execution_environment(job, environ, session_id, nest_dir, delta_env = { key: value for key, value in target_env.items() - if key not in base_env or base_env[key] != value + if key not in base_env + or base_env[key] != value or key in job.get_environ_settings() } - if 'reset-locale' in job.get_flag_set(): - delta_env['LANG'] = 'C.UTF-8' - delta_env['LANGUAGE'] = '' - delta_env['LC_ALL'] = 'C.UTF-8' + if "reset-locale" in job.get_flag_set(): + delta_env["LANG"] = "C.UTF-8" + delta_env["LANGUAGE"] = "" + delta_env["LC_ALL"] = "C.UTF-8" if extra_env: delta_env.update(extra_env()) # Preserve the copy_vars variables + those prefixed with SNAP on Snappy - if (os.getenv("SNAP") or os.getenv("SNAP_APP_PATH")): - copy_vars = ['PYTHONHOME', 'PYTHONUSERBASE', 'LD_LIBRARY_PATH', - 'GI_TYPELIB_PATH', 'PROVIDERPATH', 'PYTHONPATH', - 'PERL5LIB'] + if os.getenv("SNAP") or os.getenv("SNAP_APP_PATH"): + copy_vars = [ + "PYTHONHOME", + "PYTHONUSERBASE", + "LD_LIBRARY_PATH", + "GI_TYPELIB_PATH", + "PROVIDERPATH", + "PYTHONPATH", + "PERL5LIB", + ] for key, value in base_env.items(): - if key in copy_vars or key.startswith('SNAP'): + if key in copy_vars or key.startswith("SNAP"): delta_env[key] = value return delta_env -def get_execution_command(job, environ, session_id, - nest_dir, target_user=None, extra_env=None): +def get_execution_command( + job, environ, session_id, nest_dir, target_user=None, extra_env=None +): """Generate a command argv to run in the shell.""" cmd = [] if target_user: @@ -565,21 +651,31 @@ def get_execution_command(job, environ, session_id, # (--reset-timestamp) # - gets password as the first line of stdin (--stdin) # - change the user to the target user (--user) - cmd = ['sudo', '--prompt', '', '--reset-timestamp', '--stdin', - '--user', target_user] - cmd += ['env'] + cmd = [ + "sudo", + "--prompt", + "", + "--reset-timestamp", + "--stdin", + "--user", + target_user, + ] + cmd += ["env"] if target_user: env = get_differential_execution_environment( - job, environ, session_id, nest_dir, extra_env) + job, environ, session_id, nest_dir, extra_env + ) else: env = get_execution_environment(job, environ, session_id, nest_dir) if extra_env: env.update(extra_env()) - cmd += ["{key}={value}".format(key=key, value=value) - for key, value in sorted(env.items())] + cmd += [ + "{key}={value}".format(key=key, value=value) + for key, value in sorted(env.items()) + ] # Run the command unconfined on ubuntu core because of snap-confine fixes # related to https://ubuntu.com/security/CVE-2021-44731 if on_ubuntucore(): - cmd += ['aa-exec', '-p', 'unconfined'] - cmd += [job.shell, '-c', job.command] + cmd += ["aa-exec", "-p", "unconfined"] + cmd += [job.shell, "-c", job.command] return cmd diff --git a/checkbox-ng/plainbox/impl/exporter/__init__.py b/checkbox-ng/plainbox/impl/exporter/__init__.py index 9a3c03773..f1957f5da 100644 --- a/checkbox-ng/plainbox/impl/exporter/__init__.py +++ b/checkbox-ng/plainbox/impl/exporter/__init__.py @@ -41,6 +41,7 @@ class classproperty: """ Class property. """ + # I wish it was in the standard library or that the composition worked def __init__(self, func): @@ -69,19 +70,19 @@ class SessionStateExporterBase(ISessionStateExporter): user interface from becoming annoying. """ - OPTION_WITH_IO_LOG = 'with-io-log' - OPTION_SQUASH_IO_LOG = 'squash-io-log' - OPTION_FLATTEN_IO_LOG = 'flatten-io-log' - OPTION_WITH_RUN_LIST = 'with-run-list' - OPTION_WITH_JOB_LIST = 'with-job-list' - OPTION_WITH_DESIRED_JOB_LIST = 'with-job-list' - OPTION_WITH_RESOURCE_MAP = 'with-resource-map' - OPTION_WITH_JOB_DEFS = 'with-job-defs' - OPTION_WITH_ATTACHMENTS = 'with-attachments' - OPTION_WITH_COMMENTS = 'with-comments' - OPTION_WITH_JOB_HASH = 'with-job-hash' - OPTION_WITH_CATEGORY_MAP = 'with-category-map' - OPTION_WITH_CERTIFICATION_STATUS = 'with-certification-status' + OPTION_WITH_IO_LOG = "with-io-log" + OPTION_SQUASH_IO_LOG = "squash-io-log" + OPTION_FLATTEN_IO_LOG = "flatten-io-log" + OPTION_WITH_RUN_LIST = "with-run-list" + OPTION_WITH_JOB_LIST = "with-job-list" + OPTION_WITH_DESIRED_JOB_LIST = "with-job-list" + OPTION_WITH_RESOURCE_MAP = "with-resource-map" + OPTION_WITH_JOB_DEFS = "with-job-defs" + OPTION_WITH_ATTACHMENTS = "with-attachments" + OPTION_WITH_COMMENTS = "with-comments" + OPTION_WITH_JOB_HASH = "with-job-hash" + OPTION_WITH_CATEGORY_MAP = "with-category-map" + OPTION_WITH_CERTIFICATION_STATUS = "with-certification-status" SUPPORTED_OPTION_LIST = ( OPTION_WITH_IO_LOG, @@ -148,9 +149,13 @@ def _option_list(self): Users who only are about whether an option is set, regardless of the value assigned to it, can use this API. """ - return sorted([ - option for option in self._option_dict.keys() - if self._option_dict[option]]) + return sorted( + [ + option + for option in self._option_dict.keys() + if self._option_dict[option] + ] + ) @_option_list.setter def _option_list(self, value): @@ -189,8 +194,12 @@ def dump_from_session_manager(self, session_manager, stream): This method takes session manager instance, extracts session information from it, and dumps it to a stream. """ - self.dump(self.get_session_data_subset( - self._trim_session_manager(session_manager)), stream) + self.dump( + self.get_session_data_subset( + self._trim_session_manager(session_manager) + ), + stream, + ) def get_session_data_subset(self, session_manager): """ @@ -203,82 +212,88 @@ def get_session_data_subset(self, session_manager): Special care must be taken when processing io_log (and in the future, attachments) as those can be arbitrarily large. """ - data = { - 'result_map': {} - } + data = {"result_map": {}} session = session_manager.state if self.OPTION_WITH_JOB_LIST in self._option_list: - data['job_list'] = [job.id for job in session.job_list] + data["job_list"] = [job.id for job in session.job_list] if self.OPTION_WITH_RUN_LIST in self._option_list: - data['run_list'] = [job.id for job in session.run_list] + data["run_list"] = [job.id for job in session.run_list] if self.OPTION_WITH_DESIRED_JOB_LIST in self._option_list: - data['desired_job_list'] = [job.id - for job in session.desired_job_list] + data["desired_job_list"] = [ + job.id for job in session.desired_job_list + ] if self.OPTION_WITH_RESOURCE_MAP in self._option_list: - data['resource_map'] = { + data["resource_map"] = { # TODO: there is no method to get all data from a Resource # instance and there probably should be. Or just let there be # a way to promote _data to a less hidden-but-non-conflicting # property. resource_name: [ object.__getattribute__(resource, "_data") - for resource in resource_list] + for resource in resource_list + ] # TODO: turn session._resource_map to a public property - for resource_name, resource_list - in session._resource_map.items() + for resource_name, resource_list in session._resource_map.items() } if self.OPTION_WITH_ATTACHMENTS in self._option_list: - data['attachment_map'] = {} + data["attachment_map"] = {} if self.OPTION_WITH_CATEGORY_MAP in self._option_list: - wanted_category_ids = frozenset({ - job_state.effective_category_id - for job_state in session.job_state_map.values() - }) - data['category_map'] = { + wanted_category_ids = frozenset( + { + job_state.effective_category_id + for job_state in session.job_state_map.values() + } + ) + data["category_map"] = { unit.id: unit.tr_name() for unit in session.unit_list - if unit.Meta.name == 'category' + if unit.Meta.name == "category" and unit.id in wanted_category_ids } # Inject the special, built-in 'uncategorized' category, if any # job needs it - UNCATEGORISED = 'com.canonical.plainbox::uncategorised' + UNCATEGORISED = "com.canonical.plainbox::uncategorised" if UNCATEGORISED in wanted_category_ids: - data['category_map'][UNCATEGORISED] = _("Uncategorised") + data["category_map"][UNCATEGORISED] = _("Uncategorised") for job_id, job_state in session.job_state_map.items(): if job_state.result.outcome is None: continue - data['result_map'][job_id] = OrderedDict() - data['result_map'][job_id]['summary'] = job_state.job.tr_summary() - data['result_map'][job_id]['category_id'] = \ - job_state.effective_category_id - data['result_map'][job_id]['outcome'] = job_state.result.outcome + data["result_map"][job_id] = OrderedDict() + data["result_map"][job_id]["summary"] = job_state.job.tr_summary() + data["result_map"][job_id][ + "category_id" + ] = job_state.effective_category_id + data["result_map"][job_id]["outcome"] = job_state.result.outcome if job_state.result.execution_duration: - data['result_map'][job_id]['execution_duration'] = \ - job_state.result.execution_duration + data["result_map"][job_id][ + "execution_duration" + ] = job_state.result.execution_duration if self.OPTION_WITH_COMMENTS in self._option_list: - data['result_map'][job_id]['comments'] = \ - job_state.result.comments + data["result_map"][job_id][ + "comments" + ] = job_state.result.comments # Add Job hash if requested if self.OPTION_WITH_JOB_HASH in self._option_list: - data['result_map'][job_id]['hash'] = job_state.job.checksum + data["result_map"][job_id]["hash"] = job_state.job.checksum # Add Job definitions if requested if self.OPTION_WITH_JOB_DEFS in self._option_list: - for prop in ('plugin', - 'requires', - 'depends', - 'command', - 'description', - ): + for prop in ( + "plugin", + "requires", + "depends", + "command", + "description", + ): if not getattr(job_state.job, prop): continue - data['result_map'][job_id][prop] = getattr( - job_state.job, prop) + data["result_map"][job_id][prop] = getattr( + job_state.job, prop + ) # Add Attachments if requested - if job_state.job.plugin == 'attachment': + if job_state.job.plugin == "attachment": if self.OPTION_WITH_ATTACHMENTS in self._option_list: self._build_attachment_map(data, job_id, job_state) continue # Don't add attachments IO logs to the result_map @@ -289,50 +304,63 @@ def get_session_data_subset(self, session_manager): # saved, discarding stream name and the relative timestamp. if self.OPTION_SQUASH_IO_LOG in self._option_list: io_log_data = self._squash_io_log( - job_state.result.get_io_log()) + job_state.result.get_io_log() + ) elif self.OPTION_FLATTEN_IO_LOG in self._option_list: io_log_data = self._flatten_io_log( - job_state.result.get_io_log()) + job_state.result.get_io_log() + ) else: io_log_data = self._io_log(job_state.result.get_io_log()) - data['result_map'][job_id]['io_log'] = io_log_data + data["result_map"][job_id]["io_log"] = io_log_data # Add certification status if requested if self.OPTION_WITH_CERTIFICATION_STATUS in self._option_list: - data['result_map'][job_id]['certification_status'] = ( - job_state.effective_certification_status) + data["result_map"][job_id][ + "certification_status" + ] = job_state.effective_certification_status return data def _build_attachment_map(self, data, job_id, job_state): - raw_bytes = b''.join( - (record[2] for record in - job_state.result.get_io_log() - if record[1] == 'stdout')) - data['attachment_map'][job_id] = \ - base64.standard_b64encode(raw_bytes).decode('ASCII') + raw_bytes = b"".join( + ( + record[2] + for record in job_state.result.get_io_log() + if record[1] == "stdout" + ) + ) + data["attachment_map"][job_id] = base64.standard_b64encode( + raw_bytes + ).decode("ASCII") @classmethod def _squash_io_log(cls, io_log): # Squash the IO log by discarding everything except for the 'data' # portion. The actual data is escaped with base64. return [ - base64.standard_b64encode(record.data).decode('ASCII') - for record in io_log] + base64.standard_b64encode(record.data).decode("ASCII") + for record in io_log + ] @classmethod def _flatten_io_log(cls, io_log): # Similar to squash but also coalesce all records into one big base64 # string (there are no arrays / lists anymore) return base64.standard_b64encode( - b''.join([record.data for record in io_log]) - ).decode('ASCII') + b"".join([record.data for record in io_log]) + ).decode("ASCII") @classmethod def _io_log(cls, io_log): # Return the raw io log, but escape the data portion with base64 - return [(record.delay, record.stream_name, - base64.standard_b64encode(record.data).decode('ASCII')) - for record in io_log] + return [ + ( + record.delay, + record.stream_name, + base64.standard_b64encode(record.data).decode("ASCII"), + ) + for record in io_log + ] @staticmethod def _trim_session_manager(session_manager): @@ -343,13 +371,16 @@ def _trim_session_manager(session_manager): jobs_to_remove = [] for job_id, job_state in session_manager.state.job_state_map.items(): # remove skipped salvage jobs - if (job_state.job.salvages is not None and - job_state.result.outcome == 'not-supported'): + if ( + job_state.job.salvages is not None + and job_state.result.outcome == "not-supported" + ): jobs_to_remove.append(job_id) continue for job_id in jobs_to_remove: session_manager.state.run_list.remove( - session_manager.state.job_state_map[job_id].job) + session_manager.state.job_state_map[job_id].job + ) del session_manager.state.job_state_map[job_id] return session_manager @@ -383,18 +414,25 @@ def __init__(self, dest_stream, encoding): self.encoding = encoding def write(self, data): - """ Writes to the stream, takes bytes and decodes them per the - object's specified encoding prior to writing. - :param data: the chunk of data to write. + """Writes to the stream, takes bytes and decodes them per the + object's specified encoding prior to writing. + :param data: the chunk of data to write. """ - if (self.dest_stream.encoding and - self.dest_stream.encoding.lower() != self.encoding.lower()): + if ( + self.dest_stream.encoding + and self.dest_stream.encoding.lower() != self.encoding.lower() + ): logger.warning( - _("Incorrect stream encoding. Got %s, expected %s. " - " some characters won't be printed"), - self.dest_stream.encoding, self.encoding) + _( + "Incorrect stream encoding. Got %s, expected %s. " + " some characters won't be printed" + ), + self.dest_stream.encoding, + self.encoding, + ) # fall back to ASCII encoding - return self.dest_stream.write(data.decode( - 'ascii', errors='ignore')) + return self.dest_stream.write( + data.decode("ascii", errors="ignore") + ) return self.dest_stream.write(data.decode(self.encoding)) diff --git a/checkbox-ng/plainbox/impl/exporter/jinja2.py b/checkbox-ng/plainbox/impl/exporter/jinja2.py index ebe4389e4..7b76a2260 100644 --- a/checkbox-ng/plainbox/impl/exporter/jinja2.py +++ b/checkbox-ng/plainbox/impl/exporter/jinja2.py @@ -34,6 +34,7 @@ import jinja2 from jinja2 import Environment from jinja2 import FileSystemLoader + try: from jinja2 import escape from jinja2.filters import environmentfilter as pass_environment @@ -51,7 +52,7 @@ #: Name-space prefix for Canonical Certification -CERTIFICATION_NS = 'com.canonical.certification::' +CERTIFICATION_NS = "com.canonical.certification::" @pass_environment @@ -67,29 +68,33 @@ def do_strip_ns(_environment, unit_id, ns=CERTIFICATION_NS): def do_is_name(text): """A filter for checking if something is equal to "name".""" - return text == 'name' + return text == "name" def json_load_ordered_dict(text): """Render json dict in Jinja templates but keep keys ordering.""" - return json.loads( - text, object_pairs_hook=OrderedDict) + return json.loads(text, object_pairs_hook=OrderedDict) def highlight_keys(text): """A filter for rendering keys as bold html text.""" - return re.sub(r'(\w+:\s)', r'\1', text) + return re.sub(r"(\w+:\s)", r"\1", text) class Jinja2SessionStateExporter(SessionStateExporterBase): - """Session state exporter that renders output using jinja2 template.""" - supported_option_list = ('without-session-desc') - - def __init__(self, option_list=None, system_id="", timestamp=None, - client_version=None, client_name='plainbox', - exporter_unit=None): + supported_option_list = "without-session-desc" + + def __init__( + self, + option_list=None, + system_id="", + timestamp=None, + client_version=None, + client_name="plainbox", + exporter_unit=None, + ): """ Initialize a new Jinja2SessionStateExporter with given arguments. """ @@ -97,8 +102,9 @@ def __init__(self, option_list=None, system_id="", timestamp=None, self._unit = exporter_unit self._system_id = system_id # Generate a time-stamp if needed - self._timestamp = ( - timestamp or datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")) + self._timestamp = timestamp or datetime.utcnow().strftime( + "%Y-%m-%dT%H:%M:%S" + ) # Use current version unless told otherwise self._client_version = client_version or get_version_string() # Remember client name @@ -116,15 +122,17 @@ def __init__(self, option_list=None, system_id="", timestamp=None, if "extra_paths" in self.data: paths.extend(self.data["extra_paths"]) self.option_list = tuple(exporter_unit.option_list or ()) + tuple( - option_list or ()) + option_list or () + ) loader = FileSystemLoader(paths) # For jinja2 version > 2.9.0 autoescape functionality is built-in, # no need to add extensions - if version.parse(jinja2.__version__) >= version.parse('2.9.0'): + if version.parse(jinja2.__version__) >= version.parse("2.9.0"): env = Environment(loader=loader) else: env = Environment( - loader=loader, extensions=['jinja2.ext.autoescape']) + loader=loader, extensions=["jinja2.ext.autoescape"] + ) self.customize_environment(env) def include_file(name): @@ -132,7 +140,7 @@ def include_file(name): # templates without parsing them. return Markup(loader.get_source(env, name)[0]) - env.globals['include_file'] = include_file + env.globals["include_file"] = include_file self.template = env.get_template(exporter_unit.template) @property @@ -148,11 +156,11 @@ def unit(self): def customize_environment(self, env): """Register filters and tests custom to the JSON exporter.""" env.autoescape = True - env.filters['jsonify'] = json.dumps - env.filters['strip_ns'] = do_strip_ns - env.filters['json_load_ordered_dict'] = json_load_ordered_dict - env.filters['highlight_keys'] = highlight_keys - env.tests['is_name'] = do_is_name + env.filters["jsonify"] = json.dumps + env.filters["strip_ns"] = do_strip_ns + env.filters["json_load_ordered_dict"] = json_load_ordered_dict + env.filters["highlight_keys"] = highlight_keys + env.tests["is_name"] = do_is_name def dump(self, data, stream): """ @@ -164,7 +172,7 @@ def dump(self, data, stream): Byte stream to write to. """ - self.template.stream(data).dump(stream, encoding='utf-8') + self.template.stream(data).dump(stream, encoding="utf-8") def dump_from_session_manager(self, session_manager, stream): """ @@ -184,14 +192,14 @@ def dump_from_session_manager(self, session_manager, stream): except ValueError: app_blob_data = {} data = { - 'OUTCOME_METADATA_MAP': OUTCOME_METADATA_MAP, - 'client_name': self._client_name, - 'client_version': self._client_version, - 'manager': session_manager, - 'app_blob': app_blob_data, - 'options': self.option_list, - 'system_id': self._system_id, - 'timestamp': self._timestamp, + "OUTCOME_METADATA_MAP": OUTCOME_METADATA_MAP, + "client_name": self._client_name, + "client_version": self._client_version, + "manager": session_manager, + "app_blob": app_blob_data, + "options": self.option_list, + "system_id": self._system_id, + "timestamp": self._timestamp, } data.update(self.data) self.dump(data, stream) @@ -209,14 +217,14 @@ def dump_from_session_manager_list(self, session_manager_list, stream): """ data = { - 'OUTCOME_METADATA_MAP': OUTCOME_METADATA_MAP, - 'client_name': self._client_name, - 'client_version': self._client_version, - 'manager_list': session_manager_list, - 'app_blob': {}, - 'options': self.option_list, - 'system_id': self._system_id, - 'timestamp': self._timestamp, + "OUTCOME_METADATA_MAP": OUTCOME_METADATA_MAP, + "client_name": self._client_name, + "client_version": self._client_version, + "manager_list": session_manager_list, + "app_blob": {}, + "options": self.option_list, + "system_id": self._system_id, + "timestamp": self._timestamp, } data.update(self.data) self.dump(data, stream) @@ -224,8 +232,8 @@ def dump_from_session_manager_list(self, session_manager_list, stream): def get_session_data_subset(self, session_manager): """Compute a subset of session data.""" return { - 'manager': session_manager, - 'options': self.option_list, + "manager": session_manager, + "options": self.option_list, } def validate(self, stream): @@ -233,7 +241,7 @@ def validate(self, stream): pos = stream.tell() stream.seek(0) validator_fun = { - 'json': self.validate_json, + "json": self.validate_json, }.get(self.unit.file_extension, lambda *_: []) problems = validator_fun(stream) # XXX: in case of problems we don't really need to .seek() back @@ -251,7 +259,7 @@ def validate_json(self, stream): try: # manually reading the stream to ensure decoding raw = stream.read() - s = raw.decode('utf-8') if type(raw) == bytes else raw + s = raw.decode("utf-8") if type(raw) == bytes else raw json.loads(s) return [] except Exception as exc: diff --git a/checkbox-ng/plainbox/impl/exporter/tar.py b/checkbox-ng/plainbox/impl/exporter/tar.py index 3f508ae31..9f9baf058 100644 --- a/checkbox-ng/plainbox/impl/exporter/tar.py +++ b/checkbox-ng/plainbox/impl/exporter/tar.py @@ -53,8 +53,8 @@ def dump_from_session_manager(self, manager, stream): """ preset = None - mem_bytes = os.sysconf('SC_PAGE_SIZE') * os.sysconf('SC_PHYS_PAGES') - mem_mib = mem_bytes/(1024.**2) + mem_bytes = os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") + mem_mib = mem_bytes / (1024.0**2) # On systems with less than 1GiB of RAM, create the submission tarball # without any compression level (i.e preset=0). # See https://docs.python.org/3/library/lzma.html @@ -64,12 +64,13 @@ def dump_from_session_manager(self, manager, stream): preset = 0 job_state_map = manager.default_device_context.state.job_state_map - with tarfile.TarFile.open(None, 'w:xz', stream, preset=preset) as tar: - for fmt in ('html', 'json', 'junit'): + with tarfile.TarFile.open(None, "w:xz", stream, preset=preset) as tar: + for fmt in ("html", "json", "junit"): unit = self._get_all_exporter_units()[ - 'com.canonical.plainbox::{}'.format(fmt)] + "com.canonical.plainbox::{}".format(fmt) + ] exporter = Jinja2SessionStateExporter(exporter_unit=unit) - with SpooledTemporaryFile(max_size=102400, mode='w+b') as _s: + with SpooledTemporaryFile(max_size=102400, mode="w+b") as _s: exporter.dump_from_session_manager(manager, _s) tarinfo = tarfile.TarInfo(name="submission.{}".format(fmt)) tarinfo.size = _s.tell() @@ -82,17 +83,20 @@ def dump_from_session_manager(self, manager, stream): recordname = job_state.result.io_log_filename except AttributeError: continue - for stdstream in ('stdout', 'stderr'): - filename = recordname.replace('record.gz', stdstream) - folder = 'test_output' - if job_state.job.plugin == 'attachment': - folder = 'attachment_files' + for stdstream in ("stdout", "stderr"): + filename = recordname.replace("record.gz", stdstream) + folder = "test_output" + if job_state.job.plugin == "attachment": + folder = "attachment_files" if os.path.exists(filename) and os.path.getsize(filename): arcname = os.path.basename(filename) - if stdstream == 'stdout': + if stdstream == "stdout": arcname = os.path.splitext(arcname)[0] - tar.add(filename, os.path.join(folder, arcname), - recursive=False) + tar.add( + filename, + os.path.join(folder, arcname), + recursive=False, + ) def dump(self, session, stream): pass @@ -101,6 +105,6 @@ def _get_all_exporter_units(self): exporter_map = {} for provider in get_providers(): for unit in provider.unit_list: - if unit.Meta.name == 'exporter': + if unit.Meta.name == "exporter": exporter_map[unit.id] = ExporterUnitSupport(unit) return exporter_map diff --git a/checkbox-ng/plainbox/impl/exporter/test_html.py b/checkbox-ng/plainbox/impl/exporter/test_html.py index 873b5cf5a..a2b7c64ee 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_html.py +++ b/checkbox-ng/plainbox/impl/exporter/test_html.py @@ -42,63 +42,78 @@ class HTMLExporterTests(TestCase): - """Tests for Jinja2SessionStateExporter using the HTML template.""" def setUp(self): self.exporter_unit = self._get_all_exporter_units()[ - 'com.canonical.plainbox::html'] + "com.canonical.plainbox::html" + ] self.resource_map = { - 'com.canonical.certification::lsb': [ - Resource({'description': 'Ubuntu 14.04 LTS'})], - 'com.canonical.certification::package': [ - Resource({'name': 'plainbox', 'version': '1.0'}), - Resource({'name': 'fwts', 'version': '0.15.2'})], - } - self.job1 = JobDefinition({'id': 'job_id1', '_summary': 'job 1'}) - self.job2 = JobDefinition({'id': 'job_id2', '_summary': 'job 2'}) - self.job3 = JobDefinition({'id': 'job_id3', '_summary': 'job 3'}) - self.res1 = JobDefinition({'id': 'lsb', 'plugin': 'resource', - '_summary': 'lsb'}) - self.res2 = JobDefinition({'id': 'package', 'plugin': 'resource', - '_summary': 'package'}) - self.result_fail = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_FAIL, 'return_code': 1, - 'io_log': [(0, 'stderr', b'FATAL ERROR\n')], - }) - self.result_pass = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, 'return_code': 0, - 'io_log': [(0, 'stdout', b'foo\n')], - 'comments': 'blah blah' - }) - self.result_skip = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_SKIP, - 'comments': 'No such device' - }) - self.attachment = JobDefinition({ - 'id': 'dmesg_attachment', - 'plugin': 'attachment'}) - self.attachment_result = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, - 'io_log': [(0, 'stdout', b'bar\n')], - 'return_code': 0 - }) - self.res1_result = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, - 'io_log': [(0, 'stdout', b'description: Ubuntu 14.04 LTS\n')], - 'return_code': 0 - }) - self.res2_result = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, - 'io_log': [ - (0, 'stdout', b"name: plainbox\n"), - (1, 'stdout', b"version: 1.0\n"), - (2, 'stdout', b"\n"), - (3, 'stdout', b"name: fwts\n"), - (4, 'stdout', b"version: 0.15.2\n"), + "com.canonical.certification::lsb": [ + Resource({"description": "Ubuntu 14.04 LTS"}) ], - 'return_code': 0 - }) + "com.canonical.certification::package": [ + Resource({"name": "plainbox", "version": "1.0"}), + Resource({"name": "fwts", "version": "0.15.2"}), + ], + } + self.job1 = JobDefinition({"id": "job_id1", "_summary": "job 1"}) + self.job2 = JobDefinition({"id": "job_id2", "_summary": "job 2"}) + self.job3 = JobDefinition({"id": "job_id3", "_summary": "job 3"}) + self.res1 = JobDefinition( + {"id": "lsb", "plugin": "resource", "_summary": "lsb"} + ) + self.res2 = JobDefinition( + {"id": "package", "plugin": "resource", "_summary": "package"} + ) + self.result_fail = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_FAIL, + "return_code": 1, + "io_log": [(0, "stderr", b"FATAL ERROR\n")], + } + ) + self.result_pass = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "return_code": 0, + "io_log": [(0, "stdout", b"foo\n")], + "comments": "blah blah", + } + ) + self.result_skip = MemoryJobResult( + {"outcome": IJobResult.OUTCOME_SKIP, "comments": "No such device"} + ) + self.attachment = JobDefinition( + {"id": "dmesg_attachment", "plugin": "attachment"} + ) + self.attachment_result = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "io_log": [(0, "stdout", b"bar\n")], + "return_code": 0, + } + ) + self.res1_result = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "io_log": [(0, "stdout", b"description: Ubuntu 14.04 LTS\n")], + "return_code": 0, + } + ) + self.res2_result = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "io_log": [ + (0, "stdout", b"name: plainbox\n"), + (1, "stdout", b"version: 1.0\n"), + (2, "stdout", b"\n"), + (3, "stdout", b"name: fwts\n"), + (4, "stdout", b"version: 0.15.2\n"), + ], + "return_code": 0, + } + ) self.session_manager = SessionManager.create() self.session_manager.add_local_device_context() self.session_state = self.session_manager.default_device_context.state @@ -117,15 +132,17 @@ def setUp(self): session_state.update_job_result(self.res1, self.res1_result) session_state.update_job_result(self.res2, self.res2_result) session_state.update_job_result( - self.attachment, self.attachment_result) + self.attachment, self.attachment_result + ) for resource_id, resource_list in self.resource_map.items(): session_state.set_resource_list(resource_id, resource_list) def tearDown(self): self.session_manager.destroy() - def _get_session_manager(self, job1_cert_status, job2_cert_status, - job3_cert_status): + def _get_session_manager( + self, job1_cert_status, job2_cert_status, job3_cert_status + ): session_state = self.session_manager.default_device_context.state job1_state = session_state.job_state_map[self.job1.id] job2_state = session_state.job_state_map[self.job2.id] @@ -139,26 +156,30 @@ def _get_all_exporter_units(self): exporter_map = {} for provider in get_providers(): for unit in provider.unit_list: - if unit.Meta.name == 'exporter': + if unit.Meta.name == "exporter": exporter_map[unit.id] = ExporterUnitSupport(unit) return exporter_map def prepare_manager_without_certification_status(self): return self._get_session_manager( - 'unspecified', 'unspecified', 'unspecified') + "unspecified", "unspecified", "unspecified" + ) def prepare_manager_with_certification_blocker(self): return self._get_session_manager( - 'blocker', 'unspecified', 'unspecified') + "blocker", "unspecified", "unspecified" + ) def prepare_manager_with_certification_non_blocker(self): return self._get_session_manager( - 'non-blocker', 'unspecified', 'unspecified') + "non-blocker", "unspecified", "unspecified" + ) def prepare_manager_with_both_certification_status(self): self.session_state.update_job_result(self.job2, self.result_fail) return self._get_session_manager( - 'blocker', 'non-blocker', 'unspecified') + "blocker", "non-blocker", "unspecified" + ) def test_perfect_match_without_certification_status(self): """ @@ -169,15 +190,17 @@ def test_perfect_match_without_certification_status(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", - exporter_unit=self.exporter_unit) + exporter_unit=self.exporter_unit, + ) stream = io.BytesIO() exporter.dump_from_session_manager( - self.prepare_manager_without_certification_status(), stream) + self.prepare_manager_without_certification_status(), stream + ) actual_result = stream.getvalue() # This is bytes self.assertIsInstance(actual_result, bytes) expected_result = resource_string( "plainbox", - "test-data/html-exporter/without_certification_status.html" + "test-data/html-exporter/without_certification_status.html", ) # unintuitively, resource_string returns bytes self.assertEqual(actual_result, expected_result) @@ -190,15 +213,17 @@ def test_perfect_match_with_certification_blocker(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", - exporter_unit=self.exporter_unit) + exporter_unit=self.exporter_unit, + ) stream = io.BytesIO() exporter.dump_from_session_manager( - self.prepare_manager_with_certification_blocker(), stream) + self.prepare_manager_with_certification_blocker(), stream + ) actual_result = stream.getvalue() # This is bytes self.assertIsInstance(actual_result, bytes) expected_result = resource_string( "plainbox", - "test-data/html-exporter/with_certification_blocker.html" + "test-data/html-exporter/with_certification_blocker.html", ) # unintuitively, resource_string returns bytes self.assertEqual(actual_result, expected_result) @@ -211,15 +236,17 @@ def test_perfect_match_with_certification_non_blocker(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", - exporter_unit=self.exporter_unit) + exporter_unit=self.exporter_unit, + ) stream = io.BytesIO() exporter.dump_from_session_manager( - self.prepare_manager_with_certification_non_blocker(), stream) + self.prepare_manager_with_certification_non_blocker(), stream + ) actual_result = stream.getvalue() # This is bytes self.assertIsInstance(actual_result, bytes) expected_result = resource_string( "plainbox", - "test-data/html-exporter/with_certification_non_blocker.html" + "test-data/html-exporter/with_certification_non_blocker.html", ) # unintuitively, resource_string returns bytes self.assertEqual(actual_result, expected_result) @@ -232,14 +259,16 @@ def test_perfect_match_with_both_certification_status(self): system_id="", timestamp="2012-12-21T12:00:00", client_version="Checkbox 1.0", - exporter_unit=self.exporter_unit) + exporter_unit=self.exporter_unit, + ) stream = io.BytesIO() exporter.dump_from_session_manager( - self.prepare_manager_with_both_certification_status(), stream) + self.prepare_manager_with_both_certification_status(), stream + ) actual_result = stream.getvalue() # This is bytes self.assertIsInstance(actual_result, bytes) expected_result = resource_string( "plainbox", - "test-data/html-exporter/with_both_certification_status.html" + "test-data/html-exporter/with_both_certification_status.html", ) # unintuitively, resource_string returns bytes self.assertEqual(actual_result, expected_result) diff --git a/checkbox-ng/plainbox/impl/exporter/test_init.py b/checkbox-ng/plainbox/impl/exporter/test_init.py index 8980390b8..f92beb3b7 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_init.py +++ b/checkbox-ng/plainbox/impl/exporter/test_init.py @@ -77,8 +77,8 @@ def dump(self, data, stream): def make_test_session(self): # Create a small session with two jobs and two results - job_a = make_job('job_a') - job_b = make_job('job_b') + job_a = make_job("job_a") + job_b = make_job("job_b") session = SessionState([job_a, job_b]) session.update_desired_job_list([job_a, job_b]) result_a = make_job_result(outcome=IJobResult.OUTCOME_PASS) @@ -91,25 +91,35 @@ def test_option_list_setting_boolean(self): exporter = self.TestSessionStateExporter() exporter._option_list = [ SessionStateExporterBase.OPTION_WITH_IO_LOG, - SessionStateExporterBase.OPTION_FLATTEN_IO_LOG] - self.assertEqual(exporter._option_list, sorted([ - SessionStateExporterBase.OPTION_WITH_IO_LOG, - SessionStateExporterBase.OPTION_FLATTEN_IO_LOG])) + SessionStateExporterBase.OPTION_FLATTEN_IO_LOG, + ] + self.assertEqual( + exporter._option_list, + sorted( + [ + SessionStateExporterBase.OPTION_WITH_IO_LOG, + SessionStateExporterBase.OPTION_FLATTEN_IO_LOG, + ] + ), + ) def test_option_list_setting_boolean_all_at_once(self): # Test every option set, all at once # Just to be paranoid, ensure the options I set are the ones the # exporter actually thinks it has exporter = self.TestSessionStateExporter( - self.TestSessionStateExporter.supported_option_list) + self.TestSessionStateExporter.supported_option_list + ) self.assertEqual( exporter._option_list, - sorted(self.TestSessionStateExporter.supported_option_list)) + sorted(self.TestSessionStateExporter.supported_option_list), + ) def test_option_list_init_non_boolean(self): option = SessionStateExporterBase.OPTION_WITH_COMMENTS exporter = self.TestSessionStateExporter( - ["{}=detailed".format(option)]) + ["{}=detailed".format(option)] + ) self.assertEqual(exporter.get_option_value(option), "detailed") def test_option_list_non_duplicated_options(self): @@ -121,33 +131,44 @@ def test_option_list_non_duplicated_options(self): def test_option_list_setting_api(self): exporter = self.TestSessionStateExporter( - [SessionStateExporterBase.OPTION_WITH_IO_LOG]) + [SessionStateExporterBase.OPTION_WITH_IO_LOG] + ) exporter.set_option_value("with-comments") - self.assertEqual(exporter.get_option_value('with-comments'), True) + self.assertEqual(exporter.get_option_value("with-comments"), True) exporter.set_option_value("with-comments", "detailed") - self.assertEqual(exporter.get_option_value('with-comments'), - "detailed") + self.assertEqual( + exporter.get_option_value("with-comments"), "detailed" + ) def test_defaults(self): # Test all defaults, with all options unset exporter = self.TestSessionStateExporter() - session_manager = mock.Mock(spec_set=SessionManager, - state=self.make_test_session()) + session_manager = mock.Mock( + spec_set=SessionManager, state=self.make_test_session() + ) data = exporter.get_session_data_subset(session_manager) expected_data = { - 'result_map': { - 'job_a': OrderedDict([ - ('summary', 'job_a'), - ('category_id', ('com.canonical.plainbox::' - 'uncategorised')), - ('outcome', 'pass') - ]), - 'job_b': OrderedDict([ - ('summary', 'job_b'), - ('category_id', ('com.canonical.plainbox::' - 'uncategorised')), - ('outcome', 'fail') - ]) + "result_map": { + "job_a": OrderedDict( + [ + ("summary", "job_a"), + ( + "category_id", + ("com.canonical.plainbox::" "uncategorised"), + ), + ("outcome", "pass"), + ] + ), + "job_b": OrderedDict( + [ + ("summary", "job_b"), + ( + "category_id", + ("com.canonical.plainbox::" "uncategorised"), + ), + ("outcome", "fail"), + ] + ), } } self.assertEqual(data, expected_data) @@ -155,32 +176,40 @@ def test_defaults(self): def make_realistic_test_session(self, session_dir): # Create a more realistic session with two jobs but with richer set # of data in the actual jobs and results. - job_a = JobDefinition({ - 'plugin': 'shell', - 'name': 'job_a', - 'summary': 'This is job A', - 'command': 'echo testing && true', - 'requires': 'job_b.ready == "yes"' - }) - job_b = JobDefinition({ - 'plugin': 'resource', - 'name': 'job_b', - 'summary': 'This is job B', - 'command': 'echo ready: yes' - }) + job_a = JobDefinition( + { + "plugin": "shell", + "name": "job_a", + "summary": "This is job A", + "command": "echo testing && true", + "requires": 'job_b.ready == "yes"', + } + ) + job_b = JobDefinition( + { + "plugin": "resource", + "name": "job_b", + "summary": "This is job B", + "command": "echo ready: yes", + } + ) session = SessionState([job_a, job_b]) session.update_desired_job_list([job_a, job_b]) - result_a = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, - 'return_code': 0, - 'io_log': [(0, 'stdout', b'testing\n')], - }) - result_b = MemoryJobResult({ - 'outcome': IJobResult.OUTCOME_PASS, - 'return_code': 0, - 'comments': 'foo', - 'io_log': [(0, 'stdout', b'ready: yes\n')], - }) + result_a = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "return_code": 0, + "io_log": [(0, "stdout", b"testing\n")], + } + ) + result_b = MemoryJobResult( + { + "outcome": IJobResult.OUTCOME_PASS, + "return_code": 0, + "comments": "foo", + "io_log": [(0, "stdout", b"ready: yes\n")], + } + ) session.update_job_result(job_a, result_a) session.update_job_result(job_b, result_b) return session @@ -195,60 +224,72 @@ def test_all_at_once(self): # and thus the code below tests that option with TemporaryDirectory() as scratch_dir: exporter = self.TestSessionStateExporter( - self.TestSessionStateExporter.supported_option_list) + self.TestSessionStateExporter.supported_option_list + ) session_manager = mock.Mock( spec_set=SessionManager, - state=self.make_realistic_test_session(scratch_dir)) + state=self.make_realistic_test_session(scratch_dir), + ) data = exporter.get_session_data_subset(session_manager) expected_data = { - 'job_list': ['job_a', 'job_b'], - 'run_list': ['job_b', 'job_a'], - 'desired_job_list': ['job_a', 'job_b'], - 'resource_map': { - 'job_b': [{ - 'ready': 'yes' - }] + "job_list": ["job_a", "job_b"], + "run_list": ["job_b", "job_a"], + "desired_job_list": ["job_a", "job_b"], + "resource_map": {"job_b": [{"ready": "yes"}]}, + "category_map": { + "com.canonical.plainbox::uncategorised": "Uncategorised" }, - 'category_map': { - 'com.canonical.plainbox::uncategorised': 'Uncategorised' + "result_map": { + "job_a": OrderedDict( + [ + ("summary", "This is job A"), + ( + "category_id", + ("com.canonical.plainbox::" "uncategorised"), + ), + ("outcome", "pass"), + ("comments", None), + ( + "hash", + "2def0c995e1b6d934c5a91286ba164" + "18845da26d057bc992a2b5dfeae2e2fe91", + ), + ("plugin", "shell"), + ("requires", 'job_b.ready == "yes"'), + ("command", "echo testing && true"), + ("io_log", ["dGVzdGluZwo="]), + ("certification_status", "unspecified"), + ] + ), + "job_b": OrderedDict( + [ + ("summary", "This is job B"), + ( + "category_id", + ("com.canonical.plainbox::" "uncategorised"), + ), + ("outcome", "pass"), + ("comments", "foo"), + ( + "hash", + "ed19ba54624864a7c622ff7d1e8ed5" + "96b1a0fddc4b78c8fb780fe41e55250e6f", + ), + ("plugin", "resource"), + ("command", "echo ready: yes"), + ("io_log", ["cmVhZHk6IHllcwo="]), + ("certification_status", "unspecified"), + ] + ), }, - 'result_map': { - 'job_a': OrderedDict([ - ('summary', 'This is job A'), - ('category_id', ('com.canonical.plainbox::' - 'uncategorised')), - ('outcome', 'pass'), - ('comments', None), - ('hash', '2def0c995e1b6d934c5a91286ba164' - '18845da26d057bc992a2b5dfeae2e2fe91'), - ('plugin', 'shell'), - ('requires', 'job_b.ready == "yes"'), - ('command', 'echo testing && true'), - ('io_log', ['dGVzdGluZwo=']), - ('certification_status', 'unspecified'), - ]), - 'job_b': OrderedDict([ - ('summary', 'This is job B'), - ('category_id', ('com.canonical.plainbox::' - 'uncategorised')), - ('outcome', 'pass'), - ('comments', 'foo'), - ('hash', 'ed19ba54624864a7c622ff7d1e8ed5' - '96b1a0fddc4b78c8fb780fe41e55250e6f'), - ('plugin', 'resource'), - ('command', 'echo ready: yes'), - ('io_log', ['cmVhZHk6IHllcwo=']), - ('certification_status', 'unspecified'), - ]) - }, - 'attachment_map': { - } + "attachment_map": {}, } # This is just to make debugging easier self.assertEqual(expected_data.keys(), data.keys()) for key in data.keys(): - self.assertEqual(expected_data[key], data[key], - msg="wrong data in %r" % key) + self.assertEqual( + expected_data[key], data[key], msg="wrong data in %r" % key + ) # This is to make sure we didn't miss anything by being too smart self.assertEqual(data, expected_data) @@ -257,86 +298,97 @@ def test_io_log_processors(self): # the base SessionStateExporter class cls = self.TestSessionStateExporter io_log = ( - IOLogRecord(0, 'stdout', b'foo\n'), - IOLogRecord(1, 'stderr', b'bar\n'), - IOLogRecord(2, 'stdout', b'quxx\n') + IOLogRecord(0, "stdout", b"foo\n"), + IOLogRecord(1, "stderr", b"bar\n"), + IOLogRecord(2, "stdout", b"quxx\n"), ) self.assertEqual( - cls._squash_io_log(io_log), [ - 'Zm9vCg==', 'YmFyCg==', 'cXV4eAo=']) - self.assertEqual( - cls._flatten_io_log(io_log), - 'Zm9vCmJhcgpxdXh4Cg==') + cls._squash_io_log(io_log), ["Zm9vCg==", "YmFyCg==", "cXV4eAo="] + ) + self.assertEqual(cls._flatten_io_log(io_log), "Zm9vCmJhcgpxdXh4Cg==") self.assertEqual( - cls._io_log(io_log), [ - (0, 'stdout', 'Zm9vCg=='), - (1, 'stderr', 'YmFyCg=='), - (2, 'stdout', 'cXV4eAo=')]) + cls._io_log(io_log), + [ + (0, "stdout", "Zm9vCg=="), + (1, "stderr", "YmFyCg=="), + (2, "stdout", "cXV4eAo="), + ], + ) def test_category_map(self): """ Ensure that passing OPTION_WITH_CATEGORY_MAP causes a category id -> tr_name mapping to show up. """ - exporter = self.TestSessionStateExporter([ - SessionStateExporterBase.OPTION_WITH_CATEGORY_MAP - ]) + exporter = self.TestSessionStateExporter( + [SessionStateExporterBase.OPTION_WITH_CATEGORY_MAP] + ) # Create three untis, two categories (foo, bar) and two jobs (froz, # bot) so that froz.category_id == foo - cat_foo = CategoryUnit({ - 'id': 'foo', - 'name': 'The foo category', - }) - cat_bar = CategoryUnit({ - 'id': 'bar', - 'name': 'The bar category', - }) - job_froz = JobDefinition({ - 'plugin': 'shell', - 'id': 'froz', - 'category_id': 'foo' - }) + cat_foo = CategoryUnit( + { + "id": "foo", + "name": "The foo category", + } + ) + cat_bar = CategoryUnit( + { + "id": "bar", + "name": "The bar category", + } + ) + job_froz = JobDefinition( + {"plugin": "shell", "id": "froz", "category_id": "foo"} + ) # Create and export a session with the three units state = SessionState([cat_foo, cat_bar, job_froz]) session_manager = mock.Mock(spec_set=SessionManager, state=state) data = exporter.get_session_data_subset(session_manager) # Ensure that only the foo category was used, and the bar category was # discarded as nothing was referencing it - self.assertEqual(data['category_map'], { - 'foo': 'The foo category', - }) + self.assertEqual( + data["category_map"], + { + "foo": "The foo category", + }, + ) def test_category_map_and_uncategorised(self): """ Ensure that OPTION_WITH_CATEGORY_MAP synthetizes the special 'uncategorised' category. """ - exporter = self.TestSessionStateExporter([ - SessionStateExporterBase.OPTION_WITH_CATEGORY_MAP - ]) + exporter = self.TestSessionStateExporter( + [SessionStateExporterBase.OPTION_WITH_CATEGORY_MAP] + ) # Create a job without a specific category - job = JobDefinition({ - 'plugin': 'shell', - 'id': 'id', - }) + job = JobDefinition( + { + "plugin": "shell", + "id": "id", + } + ) # Create and export a session with that one job state = SessionState([job]) session_manager = mock.Mock(spec_set=SessionManager, state=state) data = exporter.get_session_data_subset(session_manager) # Ensure that the special 'uncategorized' category is used - self.assertEqual(data['category_map'], { - 'com.canonical.plainbox::uncategorised': 'Uncategorised', - }) + self.assertEqual( + data["category_map"], + { + "com.canonical.plainbox::uncategorised": "Uncategorised", + }, + ) class ByteStringStreamTranslatorTests(TestCase): def test_smoke(self): dest_stream = StringIO() - source_stream = BytesIO(b'This is a bytes literal') - encoding = 'utf-8' + source_stream = BytesIO(b"This is a bytes literal") + encoding = "utf-8" translator = ByteStringStreamTranslator(dest_stream, encoding) translator.write(source_stream.getvalue()) - self.assertEqual('This is a bytes literal', dest_stream.getvalue()) + self.assertEqual("This is a bytes literal", dest_stream.getvalue()) diff --git a/checkbox-ng/plainbox/impl/exporter/test_jinja2.py b/checkbox-ng/plainbox/impl/exporter/test_jinja2.py index 21809ead0..2c7e0bde2 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_jinja2.py +++ b/checkbox-ng/plainbox/impl/exporter/test_jinja2.py @@ -43,53 +43,55 @@ def setUp(self): self.prepare_manager_single_job() def prepare_manager_single_job(self): - result = mock.Mock(spec_set=MemoryJobResult, outcome='fail', - is_hollow=False) - result.tr_outcome.return_value = 'fail' - job = mock.Mock(spec_set=JobDefinition, id='job_id') - job.tr_summary.return_value = 'job name' - self.manager_single_job = mock.Mock(state=mock.Mock( - metadata=SessionMetaData(), - run_list=[job], - job_state_map={ - job.id: mock.Mock(result=result, job=job) - }) + result = mock.Mock( + spec_set=MemoryJobResult, outcome="fail", is_hollow=False + ) + result.tr_outcome.return_value = "fail" + job = mock.Mock(spec_set=JobDefinition, id="job_id") + job.tr_summary.return_value = "job name" + self.manager_single_job = mock.Mock( + state=mock.Mock( + metadata=SessionMetaData(), + run_list=[job], + job_state_map={job.id: mock.Mock(result=result, job=job)}, + ) ) def test_template(self): with TemporaryDirectory() as tmp: - template_filename = 'template.html' + template_filename = "template.html" pathname = os.path.join(tmp, template_filename) tmpl = dedent( "{% for job in manager.state.job_state_map %}" "{{'{:^15}: {}'.format(" "manager.state.job_state_map[job].result.tr_outcome()," "manager.state.job_state_map[job].job.tr_summary()) }}\n" - "{% endfor %}") + "{% endfor %}" + ) data = {"template": template_filename, "extra_paths": [tmp]} exporter_unit = mock.Mock(spec=ExporterUnitSupport, data=data) - exporter_unit.file_extension = 'html' + exporter_unit.file_extension = "html" exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () - with open(pathname, 'w') as f: + with open(pathname, "w") as f: f.write(tmpl) exporter = Jinja2SessionStateExporter(exporter_unit=exporter_unit) stream = BytesIO() exporter.dump_from_session_manager(self.manager_single_job, stream) - expected_bytes = ' fail : job name\n'.encode('UTF-8') + expected_bytes = " fail : job name\n".encode("UTF-8") self.assertEqual(stream.getvalue(), expected_bytes) def test_validation_chooses_json(self): - template_filename = 'template.json' + template_filename = "template.json" with TemporaryDirectory() as tmp: - tmpl = '{}' + tmpl = "{}" pathname = os.path.join(tmp, template_filename) - with open(pathname, 'w') as f: + with open(pathname, "w") as f: f.write(tmpl) data = {"template": template_filename, "extra_paths": [tmp]} exporter_unit = mock.Mock(spec=ExporterUnitSupport, data=data) - exporter_unit.file_extension = 'json' + exporter_unit.file_extension = "json" exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () @@ -100,15 +102,15 @@ def test_validation_chooses_json(self): exporter.validate_json.assert_called_once_with(stream) def test_validation_json(self): - template_filename = 'template.json' + template_filename = "template.json" with TemporaryDirectory() as tmp: tmpl = '{"valid": "json"}' pathname = os.path.join(tmp, template_filename) - with open(pathname, 'w') as f: + with open(pathname, "w") as f: f.write(tmpl) data = {"template": template_filename, "extra_paths": [tmp]} exporter_unit = mock.Mock(spec=ExporterUnitSupport, data=data) - exporter_unit.file_extension = 'json' + exporter_unit.file_extension = "json" exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () @@ -117,15 +119,15 @@ def test_validation_json(self): exporter.dump_from_session_manager(self.manager_single_job, stream) def test_validation_json_throws(self): - template_filename = 'template.json' + template_filename = "template.json" with TemporaryDirectory() as tmp: tmpl = 'very {"invalid": json}' pathname = os.path.join(tmp, template_filename) - with open(pathname, 'w') as f: + with open(pathname, "w") as f: f.write(tmpl) data = {"template": template_filename, "extra_paths": [tmp]} exporter_unit = mock.Mock(spec=ExporterUnitSupport, data=data) - exporter_unit.file_extension = 'json' + exporter_unit.file_extension = "json" exporter_unit.data_dir = tmp exporter_unit.template = template_filename exporter_unit.option_list = () @@ -133,4 +135,5 @@ def test_validation_json_throws(self): stream = BytesIO() with self.assertRaises(ExporterError): exporter.dump_from_session_manager( - self.manager_single_job, stream) + self.manager_single_job, stream + ) diff --git a/checkbox-ng/plainbox/impl/exporter/test_text.py b/checkbox-ng/plainbox/impl/exporter/test_text.py index 0d7938132..5ffbfcc49 100644 --- a/checkbox-ng/plainbox/impl/exporter/test_text.py +++ b/checkbox-ng/plainbox/impl/exporter/test_text.py @@ -37,17 +37,17 @@ class TextSessionStateExporterTests(TestCase): def test_default_dump(self): exporter = TextSessionStateExporter(color=False) # Text exporter expects this data format - result = mock.Mock(result='fail', is_hollow=False) - result.tr_outcome.return_value = 'fail' - job = mock.Mock(id='job_id') - job.tr_summary.return_value = 'job name' + result = mock.Mock(result="fail", is_hollow=False) + result.tr_outcome.return_value = "fail" + job = mock.Mock(id="job_id") + job.tr_summary.return_value = "job name" data = mock.Mock( run_list=[job], job_state_map={ job.id: mock.Mock(result=result, job=job, result_history=()) - } + }, ) stream = BytesIO() exporter.dump(data, stream) - expected_bytes = ' fail : job name\n'.encode('UTF-8') + expected_bytes = " fail : job name\n".encode("UTF-8") self.assertEqual(stream.getvalue(), expected_bytes) diff --git a/checkbox-ng/plainbox/impl/exporter/text.py b/checkbox-ng/plainbox/impl/exporter/text.py index 14b4cca77..9cc606897 100644 --- a/checkbox-ng/plainbox/impl/exporter/text.py +++ b/checkbox-ng/plainbox/impl/exporter/text.py @@ -32,7 +32,6 @@ class TextSessionStateExporter(SessionStateExporterBase): - """Human-readable session state exporter.""" def __init__(self, option_list=None, color=None, exporter_unit=None): @@ -53,26 +52,40 @@ def dump(self, session, stream): " {}: {}\n".format( self.C.custom( outcome_meta(state.result.outcome).unicode_sigil, - outcome_meta(state.result.outcome).color_ansi - ), state.job.tr_summary(), - ).encode("UTF-8")) + outcome_meta(state.result.outcome).color_ansi, + ), + state.job.tr_summary(), + ).encode("UTF-8") + ) if len(state.result_history) > 1: - stream.write(_(" history: {0}\n").format( - ', '.join( - self.C.custom( - result.outcome_meta().tr_outcome, - result.outcome_meta().color_ansi) - for result in state.result_history) - ).encode("UTF-8")) + stream.write( + _(" history: {0}\n") + .format( + ", ".join( + self.C.custom( + result.outcome_meta().tr_outcome, + result.outcome_meta().color_ansi, + ) + for result in state.result_history + ) + ) + .encode("UTF-8") + ) else: stream.write( "{:^15}: {}\n".format( state.result.tr_outcome(), state.job.tr_summary(), - ).encode("UTF-8")) + ).encode("UTF-8") + ) if state.result_history: - print(_("History:"), ', '.join( - self.C.custom( - result.outcome_meta().unicode_sigil, - result.outcome_meta().color_ansi) - for result in state.result_history).encode("UTF-8")) + print( + _("History:"), + ", ".join( + self.C.custom( + result.outcome_meta().unicode_sigil, + result.outcome_meta().color_ansi, + ) + for result in state.result_history + ).encode("UTF-8"), + ) diff --git a/checkbox-ng/plainbox/impl/exporter/xlsx.py b/checkbox-ng/plainbox/impl/exporter/xlsx.py index fff7eeded..1c45bc716 100644 --- a/checkbox-ng/plainbox/impl/exporter/xlsx.py +++ b/checkbox-ng/plainbox/impl/exporter/xlsx.py @@ -61,11 +61,11 @@ class XLSXSessionStateExporter(SessionStateExporterBase): * com.canonical.certification::package """ - OPTION_WITH_SYSTEM_INFO = 'with-sys-info' - OPTION_WITH_SUMMARY = 'with-summary' - OPTION_WITH_DESCRIPTION = 'with-job-description' - OPTION_WITH_TEXT_ATTACHMENTS = 'with-text-attachments' - OPTION_TEST_PLAN_EXPORT = 'tp-export' + OPTION_WITH_SYSTEM_INFO = "with-sys-info" + OPTION_WITH_SUMMARY = "with-summary" + OPTION_WITH_DESCRIPTION = "with-job-description" + OPTION_WITH_TEXT_ATTACHMENTS = "with-text-attachments" + OPTION_TEST_PLAN_EXPORT = "tp-export" SUPPORTED_OPTION_LIST = ( OPTION_WITH_SYSTEM_INFO, @@ -92,7 +92,8 @@ def __init__(self, option_list=None, exporter_unit=None): for option in exporter_unit.option_list: if option not in self.supported_option_list: raise ValueError( - _("Unsupported option: {}").format(option)) + _("Unsupported option: {}").format(option) + ) self._option_list = ( SessionStateExporterBase.OPTION_WITH_IO_LOG, SessionStateExporterBase.OPTION_FLATTEN_IO_LOG, @@ -113,170 +114,281 @@ def __init__(self, option_list=None, exporter_unit=None): def _set_formats(self): # Main Title format (Orange) - self.format01 = self.workbook.add_format({ - 'align': 'left', 'size': 24, 'font_color': '#DC4C00', - }) + self.format01 = self.workbook.add_format( + { + "align": "left", + "size": 24, + "font_color": "#DC4C00", + } + ) # Default font - self.format02 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'size': 10, - }) + self.format02 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "size": 10, + } + ) # Titles - self.format03 = self.workbook.add_format({ - 'align': 'left', 'size': 12, 'bold': 1, - }) + self.format03 = self.workbook.add_format( + { + "align": "left", + "size": 12, + "bold": 1, + } + ) # Titles + borders - self.format04 = self.workbook.add_format({ - 'align': 'left', 'size': 12, 'bold': 1, 'border': 1 - }) + self.format04 = self.workbook.add_format( + {"align": "left", "size": 12, "bold": 1, "border": 1} + ) # System info with borders - self.format05 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'border': 1, - }) + self.format05 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "border": 1, + } + ) # System info with borders, grayed out background - self.format06 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'border': 1, 'bg_color': '#E6E6E6', - }) - self.format06_2 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'border': 1, 'bg_color': '#E6E6E6', 'bold': 1, - }) + self.format06 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "border": 1, + "bg_color": "#E6E6E6", + } + ) + self.format06_2 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "border": 1, + "bg_color": "#E6E6E6", + "bold": 1, + } + ) # Headlines (center) - self.format07 = self.workbook.add_format({ - 'align': 'center', 'size': 10, 'bold': 1, - }) + self.format07 = self.workbook.add_format( + { + "align": "center", + "size": 10, + "bold": 1, + } + ) # Table rows without borders - self.format08 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - }) + self.format08 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + } + ) # Table rows without borders, grayed out background - self.format09 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'bg_color': '#E6E6E6', - }) + self.format09 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "bg_color": "#E6E6E6", + } + ) # Green background / Size 8 - self.format10 = self.workbook.add_format({ - 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'bg_color': 'lime', 'border': 1, 'border_color': 'white', - }) + self.format10 = self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "bg_color": "lime", + "border": 1, + "border_color": "white", + } + ) # Red background / Size 8 - self.format11 = self.workbook.add_format({ - 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'bg_color': 'red', 'border': 1, 'border_color': 'white', - }) + self.format11 = self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "bg_color": "red", + "border": 1, + "border_color": "white", + } + ) # Gray background / Size 8 - self.format12 = self.workbook.add_format({ - 'align': 'center', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'bg_color': 'gray', 'border': 1, 'border_color': 'white', - }) + self.format12 = self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "bg_color": "gray", + "border": 1, + "border_color": "white", + } + ) # Dictionary with formats for each possible outcome self.outcome_format_map = { - outcome_info.value: self.workbook.add_format({ - 'align': 'center', - 'valign': 'vcenter', - 'text_wrap': '1', - 'size': 8, - 'bg_color': outcome_info.color_hex, - 'border': 1, - 'border_color': 'white' - }) for outcome_info in OMM.values() + outcome_info.value: self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "text_wrap": "1", + "size": 8, + "bg_color": outcome_info.color_hex, + "border": 1, + "border_color": "white", + } + ) + for outcome_info in OMM.values() } # Attachments - self.format13 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'text_wrap': 1, 'size': 8, - 'font': 'Courier New', - }) + self.format13 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "text_wrap": 1, + "size": 8, + "font": "Courier New", + } + ) # Invisible man - self.format14 = self.workbook.add_format({'font_color': 'white'}) + self.format14 = self.workbook.add_format({"font_color": "white"}) # Headlines (left-aligned) - self.format15 = self.workbook.add_format({ - 'align': 'left', 'size': 10, 'bold': 1, - }) + self.format15 = self.workbook.add_format( + { + "align": "left", + "size": 10, + "bold": 1, + } + ) # Table rows without borders, indent level 1 - self.format16 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'size': 8, 'indent': 1, - }) + self.format16 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "size": 8, + "indent": 1, + } + ) # Table rows without borders, grayed out background, indent level 1 - self.format17 = self.workbook.add_format({ - 'align': 'left', 'valign': 'vcenter', 'size': 8, - 'bg_color': '#E6E6E6', 'indent': 1, - }) + self.format17 = self.workbook.add_format( + { + "align": "left", + "valign": "vcenter", + "size": 8, + "bg_color": "#E6E6E6", + "indent": 1, + } + ) # Table rows without borders (center) - self.format18 = self.workbook.add_format({ - 'align': 'center', 'valign': 'vcenter', 'size': 8, - }) + self.format18 = self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "size": 8, + } + ) # Table rows without borders, grayed out background (center) - self.format19 = self.workbook.add_format({ - 'align': 'center', 'valign': 'vcenter', 'size': 8, - 'bg_color': '#E6E6E6', - }) + self.format19 = self.workbook.add_format( + { + "align": "center", + "valign": "vcenter", + "size": 8, + "bg_color": "#E6E6E6", + } + ) def _hw_collection(self, data): - hw_info = defaultdict(lambda: 'NA') - resource = 'com.canonical.certification::dmi' - if resource in data['resource_map']: + hw_info = defaultdict(lambda: "NA") + resource = "com.canonical.certification::dmi" + if resource in data["resource_map"]: result = [ - '{} {} ({})'.format( - i.get('vendor'), i.get('product'), i.get('version')) + "{} {} ({})".format( + i.get("vendor"), i.get("product"), i.get("version") + ) for i in data["resource_map"][resource] - if i.get('category') == 'SYSTEM'] + if i.get("category") == "SYSTEM" + ] if result: - hw_info['platform'] = result.pop() + hw_info["platform"] = result.pop() result = [ - '{}'.format(i.get('version')) + "{}".format(i.get("version")) for i in data["resource_map"][resource] - if i.get('category') == 'BIOS'] + if i.get("category") == "BIOS" + ] if result: - hw_info['bios'] = result.pop() - resource = 'com.canonical.certification::cpuinfo' - if resource in data['resource_map']: - result = ['{} x {}'.format(i.get('model'), i.get('count')) - for i in data["resource_map"][resource]] + hw_info["bios"] = result.pop() + resource = "com.canonical.certification::cpuinfo" + if resource in data["resource_map"]: + result = [ + "{} x {}".format(i.get("model"), i.get("count")) + for i in data["resource_map"][resource] + ] if result: - hw_info['processors'] = result.pop() - resource = 'com.canonical.certification::lspci_attachment' - if resource in data['attachment_map']: - lspci = data['attachment_map'][resource] + hw_info["processors"] = result.pop() + resource = "com.canonical.certification::lspci_attachment" + if resource in data["attachment_map"]: + lspci = data["attachment_map"][resource] content = standard_b64decode(lspci.encode()).decode("UTF-8") - match = re.search(r'ISA bridge.*?:\s(?P.*?)\sLPC', content) + match = re.search( + r"ISA bridge.*?:\s(?P.*?)\sLPC", content + ) if match: - hw_info['chipset'] = match.group('chipset') + hw_info["chipset"] = match.group("chipset") match = re.search( - r'Audio device.*?:\s(?P