From 1e104ed3bf27dc4413d5070c6ff6595e768aabe0 Mon Sep 17 00:00:00 2001 From: Corey McCandless Date: Wed, 12 May 2021 13:29:08 -0400 Subject: [PATCH 1/6] Gracefully fail with user-friendly error text when unrecognized dataclass fields are detected --- bin/data.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/bin/data.py b/bin/data.py index 60e30adfa0..7f5ea31769 100644 --- a/bin/data.py +++ b/bin/data.py @@ -1,5 +1,5 @@ from enum import Enum -from dataclasses import dataclass, asdict +from dataclasses import dataclass, asdict, fields from itertools import chain import json from pathlib import Path @@ -7,8 +7,25 @@ from typing import List, Any, Dict +def _custom_dataclass_init(self, **kwargs): + names = set([f.name for f in fields(self)]) + for k, v in kwargs.items(): + if k in names: + setattr(self, k, v) + else: + raise TypeError( + f"Unrecognized field '{k}' for dataclass {self.__class__.__name__}." + "\nIf this field is valid, please add it to the dataclass in data.py." + "\nIf adding an object-type field, please create a new dataclass for it." + ) + if hasattr(self, "__post_init__"): + self.__post_init__() + + @dataclass class TrackStatus: + __init__ = _custom_dataclass_init + concept_exercises: bool = False test_runner: bool = False representer: bool = False @@ -27,11 +44,16 @@ class TestRunnerSettings: @dataclass class EditorSettings: + __init__ = _custom_dataclass_init + indent_style: IndentStyle = IndentStyle.Space indent_size: int = 4 ace_editor_language: str = "python" highlightjs_language: str = "python" +<<<<<<< HEAD +======= +>>>>>>> Gracefully fail with user-friendly error text when unrecognized dataclass fields are detected def __post_init__(self): if isinstance(self.indent_style, str): @@ -47,6 +69,8 @@ class ExerciseStatus(str, Enum): @dataclass class ExerciseFiles: + __init__ = _custom_dataclass_init + solution: List[str] test: List[str] exemplar: List[str] = None @@ -71,6 +95,8 @@ def __post_init__(self): @dataclass class ExerciseConfig: + __init__ = _custom_dataclass_init + files: ExerciseFiles authors: List[str] = None forked_from: str = None @@ -95,6 +121,8 @@ def load(cls, config_file: Path) -> "ExerciseConfig": @dataclass class ExerciseInfo: + __init__ = _custom_dataclass_init + path: Path slug: str name: str @@ -160,6 +188,8 @@ def load_config(self) -> ExerciseConfig: @dataclass class Exercises: + __init__ = _custom_dataclass_init + concept: List[ExerciseInfo] practice: List[ExerciseInfo] foregone: List[str] = None @@ -190,6 +220,8 @@ def all(self, status_filter={ExerciseStatus.Active, ExerciseStatus.Beta}): @dataclass class Concept: + __init__ = _custom_dataclass_init + uuid: str slug: str name: str @@ -197,6 +229,8 @@ class Concept: @dataclass class Feature: + __init__ = _custom_dataclass_init + title: str content: str icon: str @@ -204,6 +238,8 @@ class Feature: @dataclass class FilePatterns: + __init__ = _custom_dataclass_init + solution: List[str] test: List[str] example: List[str] @@ -212,6 +248,8 @@ class FilePatterns: @dataclass class Config: + __init__ = _custom_dataclass_init + language: str slug: str active: bool @@ -253,10 +291,15 @@ def load(cls, path="config.json"): except IOError: print(f"FAIL: {path} file not found") raise SystemExit(1) + except TypeError as e: + print(f"FAIL: {e}") + raise SystemExit(1) @dataclass class TestCaseTOML: + __init__ = _custom_dataclass_init + uuid: str description: str include: bool = True @@ -265,6 +308,8 @@ class TestCaseTOML: @dataclass class TestsTOML: + __init__ = _custom_dataclass_init + cases: Dict[str, TestCaseTOML] @classmethod From 826b140a1b80cf1dc4aac683ab03207a62434f87 Mon Sep 17 00:00:00 2001 From: Corey McCandless Date: Wed, 12 May 2021 11:47:22 -0400 Subject: [PATCH 2/6] add test_runner.average_run_time to config.json dataclass --- bin/data.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bin/data.py b/bin/data.py index 7f5ea31769..ba05dd62ba 100644 --- a/bin/data.py +++ b/bin/data.py @@ -50,10 +50,6 @@ class EditorSettings: indent_size: int = 4 ace_editor_language: str = "python" highlightjs_language: str = "python" -<<<<<<< HEAD - -======= ->>>>>>> Gracefully fail with user-friendly error text when unrecognized dataclass fields are detected def __post_init__(self): if isinstance(self.indent_style, str): From d27913bb5ea6735e34a1e3c51dc57035a540406d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 May 2021 05:48:12 +0000 Subject: [PATCH 3/6] Bump actions/checkout from 2 to 2.3.4 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4) Signed-off-by: dependabot[bot] --- .github/workflows/ci-workflow.yml | 4 ++-- .github/workflows/configlet.yml | 2 +- .github/workflows/test-runner.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 523d16cfa0..165e7fbf44 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -14,7 +14,7 @@ jobs: housekeeping: runs-on: ubuntu-16.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - name: Set up Python uses: actions/setup-python@v2.2.2 @@ -55,7 +55,7 @@ jobs: matrix: python-version: [3.6, 3.7, 3.8, 3.9] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - uses: actions/setup-python@v2.2.2 with: diff --git a/.github/workflows/configlet.yml b/.github/workflows/configlet.yml index c86ed0cf59..3d7cdecf33 100644 --- a/.github/workflows/configlet.yml +++ b/.github/workflows/configlet.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + - uses: actions/checkout@v2.3.4 - name: Fetch configlet uses: exercism/github-actions/configlet-ci@main diff --git a/.github/workflows/test-runner.yml b/.github/workflows/test-runner.yml index 4d1b7dd01f..7ba9f870fb 100644 --- a/.github/workflows/test-runner.yml +++ b/.github/workflows/test-runner.yml @@ -10,6 +10,6 @@ jobs: test-runner: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - name: Run test-runner run: docker-compose run test-runner From 52a6e4c10d8b93ba1b4c6bdc2beb774191f4eb86 Mon Sep 17 00:00:00 2001 From: Corey McCandless Date: Wed, 12 May 2021 14:01:39 -0400 Subject: [PATCH 4/6] handle positional arguments in custom dataclass init --- bin/data.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/bin/data.py b/bin/data.py index ba05dd62ba..5ed50ed241 100644 --- a/bin/data.py +++ b/bin/data.py @@ -1,16 +1,34 @@ from enum import Enum from dataclasses import dataclass, asdict, fields +import dataclasses from itertools import chain import json from pathlib import Path import toml -from typing import List, Any, Dict - - -def _custom_dataclass_init(self, **kwargs): - names = set([f.name for f in fields(self)]) +from typing import List, Any, Dict, Type + + +def _custom_dataclass_init(self, *args, **kwargs): + # print(self.__class__.__name__, "__init__") + positional_count = len([ + f.name + for f in fields(self) + if isinstance(f.default, dataclasses._MISSING_TYPE) + ]) + names = [f.name for f in fields(self)] + for i, v in enumerate(args): + k = names[i] + # print(f'setting {k}={v}') + setattr(self, k, v) + if i < positional_count - 1: + raise TypeError(f"__init__() missing {positional_count - i - 1} required positional argument") + elif i >= len(names): + raise TypeError(f"__init__() too many positional arguments given") for k, v in kwargs.items(): if k in names: + if hasattr(self, k): + raise TypeError(f"__init__() got multiple values for argument '{k}'") + # print(f'setting {k}={v}') setattr(self, k, v) else: raise TypeError( From 48b532b6e91c01a0ff22910c96b300185ff3501d Mon Sep 17 00:00:00 2001 From: Corey McCandless Date: Wed, 12 May 2021 14:17:52 -0400 Subject: [PATCH 5/6] fix handling of positional arguments and keyword arguments --- bin/data.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/bin/data.py b/bin/data.py index 5ed50ed241..f976dda928 100644 --- a/bin/data.py +++ b/bin/data.py @@ -10,32 +10,48 @@ def _custom_dataclass_init(self, *args, **kwargs): # print(self.__class__.__name__, "__init__") - positional_count = len([ - f.name - for f in fields(self) - if isinstance(f.default, dataclasses._MISSING_TYPE) - ]) names = [f.name for f in fields(self)] - for i, v in enumerate(args): - k = names[i] + used_names = set() + + # Handle positional arguments + for v in args: + try: + k = names.pop(0) + except IndexError: + raise TypeError(f"__init__() given too many positional arguments") # print(f'setting {k}={v}') setattr(self, k, v) - if i < positional_count - 1: - raise TypeError(f"__init__() missing {positional_count - i - 1} required positional argument") - elif i >= len(names): - raise TypeError(f"__init__() too many positional arguments given") + used_names.add(k) + + # Handle keyword arguments for k, v in kwargs.items(): if k in names: - if hasattr(self, k): - raise TypeError(f"__init__() got multiple values for argument '{k}'") # print(f'setting {k}={v}') setattr(self, k, v) + used_names.add(k) + elif k in used_names: + raise TypeError(f"__init__() got multiple values for argument '{k}'") else: raise TypeError( f"Unrecognized field '{k}' for dataclass {self.__class__.__name__}." "\nIf this field is valid, please add it to the dataclass in data.py." "\nIf adding an object-type field, please create a new dataclass for it." ) + + # Check for missing positional arguments + missing = [ + f"'{f.name}'" for f in fields(self) + if isinstance(f.default, dataclasses._MISSING_TYPE) and f.name not in used_names + ] + if len(missing) == 1: + raise TypeError(f"__init__() missing 1 required positional argument: {missing[0]}") + elif len(missing) == 2: + raise TypeError(f"__init__() missing 2 required positional arguments: {' and '.join(missing)}") + elif len(missing) != 0: + missing[-1] = f"and {missing[-1]}" + raise TypeError(f"__init__() missing {len(missing)} required positional arguments: {', '.join(missing)}") + + # Run post init if available if hasattr(self, "__post_init__"): self.__post_init__() From cbc76b7054b40d0d2afe1ca65c6f801534b26582 Mon Sep 17 00:00:00 2001 From: Corey McCandless Date: Wed, 12 May 2021 14:24:44 -0400 Subject: [PATCH 6/6] remove single-letter variable names --- bin/data.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/bin/data.py b/bin/data.py index f976dda928..e77ec790a9 100644 --- a/bin/data.py +++ b/bin/data.py @@ -10,38 +10,38 @@ def _custom_dataclass_init(self, *args, **kwargs): # print(self.__class__.__name__, "__init__") - names = [f.name for f in fields(self)] + names = [field.name for field in fields(self)] used_names = set() # Handle positional arguments - for v in args: + for value in args: try: - k = names.pop(0) + name = names.pop(0) except IndexError: raise TypeError(f"__init__() given too many positional arguments") # print(f'setting {k}={v}') - setattr(self, k, v) - used_names.add(k) + setattr(self, name, value) + used_names.add(name) # Handle keyword arguments - for k, v in kwargs.items(): - if k in names: + for name, value in kwargs.items(): + if name in names: # print(f'setting {k}={v}') - setattr(self, k, v) - used_names.add(k) - elif k in used_names: - raise TypeError(f"__init__() got multiple values for argument '{k}'") + setattr(self, name, value) + used_names.add(name) + elif name in used_names: + raise TypeError(f"__init__() got multiple values for argument '{name}'") else: raise TypeError( - f"Unrecognized field '{k}' for dataclass {self.__class__.__name__}." + f"Unrecognized field '{name}' for dataclass {self.__class__.__name__}." "\nIf this field is valid, please add it to the dataclass in data.py." "\nIf adding an object-type field, please create a new dataclass for it." ) # Check for missing positional arguments missing = [ - f"'{f.name}'" for f in fields(self) - if isinstance(f.default, dataclasses._MISSING_TYPE) and f.name not in used_names + f"'{field.name}'" for field in fields(self) + if isinstance(field.default, dataclasses._MISSING_TYPE) and field.name not in used_names ] if len(missing) == 1: raise TypeError(f"__init__() missing 1 required positional argument: {missing[0]}") @@ -321,8 +321,8 @@ def load(cls, path="config.json"): except IOError: print(f"FAIL: {path} file not found") raise SystemExit(1) - except TypeError as e: - print(f"FAIL: {e}") + except TypeError as ex: + print(f"FAIL: {ex}") raise SystemExit(1)