From b4bca54947435812ddb6d2c441bd1dd8b3364512 Mon Sep 17 00:00:00 2001 From: Reid Draper Date: Mon, 24 May 2021 17:43:16 -0500 Subject: [PATCH] feat: add --config cli argument (#1081) Co-authored-by: Nytelife26 --- .github/workflows/ci-danger.yml | 3 ++ proselint/command_line.py | 6 ++- proselint/tools.py | 75 +++++++++++--------------- tests/test_config_flag.py | 29 ++++++++++ tests/test_config_flag_proselintrc | 85 ++++++++++++++++++++++++++++++ 5 files changed, 153 insertions(+), 45 deletions(-) create mode 100644 tests/test_config_flag.py create mode 100644 tests/test_config_flag_proselintrc diff --git a/.github/workflows/ci-danger.yml b/.github/workflows/ci-danger.yml index e2a1e68f3..43f15f66a 100644 --- a/.github/workflows/ci-danger.yml +++ b/.github/workflows/ci-danger.yml @@ -14,6 +14,9 @@ jobs: steps: - name: "[INIT] Checkout repository" uses: actions/checkout@v2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} - name: "[INIT] Install Ruby ${{ matrix.ruby }}" uses: ruby/setup-ruby@v1.71.0 with: diff --git a/proselint/command_line.py b/proselint/command_line.py index f589a2905..705ed317e 100644 --- a/proselint/command_line.py +++ b/proselint/command_line.py @@ -95,6 +95,8 @@ def print_errors(filename, errors, output_json=False, compact=False): @click.command(context_settings=CONTEXT_SETTINGS) @click.version_option(__version__, '--version', '-v', message='%(version)s') +@click.option('--config', is_flag=False, type=click.Path(), + help="Path to configuration file.") @click.option('--debug', '-d', is_flag=True, help="Give verbose output.") @click.option('--clean', '-c', is_flag=True, help="Clear the cache.") @click.option('--json', '-j', 'output_json', is_flag=True, @@ -104,7 +106,7 @@ def print_errors(filename, errors, output_json=False, compact=False): @click.option('--compact', is_flag=True, help="Shorten output.") @click.argument('paths', nargs=-1, type=click.Path()) @close_cache_shelves_after -def proselint(paths=None, version=None, clean=None, debug=None, +def proselint(paths=None, config=None, version=None, clean=None, debug=None, output_json=None, time=None, demo=None, compact=None): """Create the CLI for proselint, a linter for prose.""" if time: @@ -138,7 +140,7 @@ def proselint(paths=None, version=None, clean=None, debug=None, else: f = click.open_file( fp, 'r', encoding="utf-8", errors="replace") - errors = lint(f, debug=debug) + errors = lint(f, debug=debug, config_file_path=config) num_errors += len(errors) print_errors(fp, errors, output_json, compact=compact) except Exception: diff --git a/proselint/tools.py b/proselint/tools.py index 0202674a8..64eaa9e31 100644 --- a/proselint/tools.py +++ b/proselint/tools.py @@ -15,20 +15,10 @@ import json import importlib -try: - import dbm -except ImportError: - import anydbm as dbm - -PY3 = sys.version_info[0] == 3 -if PY3: - string_types = str -else: - string_types = basestring, - +import dbm _cache_shelves = dict() - proselint_path = os.path.dirname(os.path.realpath(__file__)) +home_dir = os.path.expanduser("~") def close_cache_shelves(): @@ -56,13 +46,11 @@ def _get_xdg_path(variable_name, default_path): def _get_xdg_config_home(): - return _get_xdg_path('XDG_CONFIG_HOME', - os.path.join(os.path.expanduser('~'), '.config')) + return _get_xdg_path('XDG_CONFIG_HOME', os.path.join(home_dir, '.config')) def _get_xdg_cache_home(): - return _get_xdg_path('XDG_CACHE_HOME', - os.path.join(os.path.expanduser('~'), '.cache')) + return _get_xdg_path('XDG_CACHE_HOME', os.path.join(home_dir, '.cache')) def _get_cache(cachepath): @@ -100,10 +88,10 @@ def memoize(f): """Cache results of computations on disk.""" # Determine the location of the cache. cache_dirname = os.path.join(_get_xdg_cache_home(), 'proselint') - legacy_cache_dirname = os.path.join(os.path.expanduser("~"), ".proselint") + legacy_cache_dirname = os.path.join(home_dir, ".proselint") if not os.path.isdir(cache_dirname): - # Migrate the cache from the legacy path to XDG complaint location. + # Migrate the cache from the legacy path to XDG compliant location. if os.path.isdir(legacy_cache_dirname): os.rename(legacy_cache_dirname, cache_dirname) # Create the cache if it does not already exist. @@ -128,9 +116,9 @@ def wrapped(*args, **kwargs): signature += item[1].encode("utf-8") key = hashlib.sha256(signature).hexdigest() + cache = _get_cache(cachepath) try: - cache = _get_cache(cachepath) return cache[key] except KeyError: value = f(*args, **kwargs) @@ -163,14 +151,14 @@ def get_checks(options): return checks -def load_options(): +def load_options(config_file_path=None, fallbacks=[]): """Read various proselintrc files, allowing user overrides.""" possible_defaults = ( '/etc/proselintrc', os.path.join(proselint_path, '.proselintrc'), ) + user_options = {} options = {} - has_overrides = False for filename in possible_defaults: try: @@ -179,31 +167,28 @@ def load_options(): except IOError: pass - try: - user_options = json.load( - open(os.path.join(_get_xdg_config_home(), 'proselint', 'config'))) + # the user has explicitly passed in a config file + # either into the `lint' method directly, or via a + # Click command-line option + if config_file_path: + user_options = json.load(open(config_file_path)) has_overrides = True - except IOError: - pass + # try to find a config file in the default locations, + # respecting environment variables + else: + for f in fallbacks: + if os.path.exists(f): + user_options = json.load(open(f)) + has_overrides = True + break # Read user configuration from the legacy path. - if not has_overrides: - try: - user_options = json.load( - open(os.path.join(os.path.expanduser('~'), '.proselintrc'))) - has_overrides = True - except IOError: - pass - - if has_overrides: + if user_options: if 'max_errors' in user_options: options['max_errors'] = user_options['max_errors'] if 'checks' in user_options: for (key, value) in user_options['checks'].items(): - try: - options['checks'][key] = value - except KeyError: - pass + options['checks'][key] = value return options @@ -238,11 +223,15 @@ def line_and_column(text, position): position_counter += len(line) -def lint(input_file, debug=False): +def lint(input_file, debug=False, config_file_path=None): """Run the linter on the input file.""" - options = load_options() + options = load_options( + config_file_path, + [os.path.join(_get_xdg_config_home(), 'proselint', 'config'), + os.path.join(home_dir, '.proselintrc')] + ) - if isinstance(input_file, string_types): + if isinstance(input_file, str): text = input_file else: text = input_file.read() @@ -261,7 +250,7 @@ def lint(input_file, debug=False): (line, column) = line_and_column(text, start) if not is_quoted(start, text): errors += [(check, message, line, column, start, end, - end - start, "warning", replacements)] + end - start, "warning", replacements)] if len(errors) > options["max_errors"]: break diff --git a/tests/test_config_flag.py b/tests/test_config_flag.py new file mode 100644 index 000000000..3d5dc363a --- /dev/null +++ b/tests/test_config_flag.py @@ -0,0 +1,29 @@ +"""Test user option overrides using --config and load_options""" +import subprocess +from proselint.tools import load_options + + +def test_load_options_function(): + """Test load_options by specifying a user options path""" + overrides = load_options("tests/test_config_flag_proselintrc") + assert load_options()["checks"]["uncomparables.misc"] + assert not overrides["checks"]["uncomparables.misc"] + + +def test_load_fallbacks(): + """Test load_options with a fallback path""" + fallbacks = load_options(None, ["tests/test_config_flag_proselintrc"]) + assert not fallbacks["checks"]["uncomparables.misc"] + fallbacks = load_options(None, ["./.proselintrc"]) + assert fallbacks["checks"]["uncomparables.misc"] + + +def test_config_flag(): + """Test the --config CLI argument""" + output = subprocess.run(["python", "-m", "proselint", "--demo"], + stdout=subprocess.PIPE, encoding='utf-8') + assert "uncomparables.misc" in output.stdout + output = subprocess.run(["python", "-m", "proselint", "--demo", "--config", + "tests/test_config_flag_proselintrc"], + stdout=subprocess.PIPE, encoding='utf-8') + assert "uncomparables.misc" not in output.stdout diff --git a/tests/test_config_flag_proselintrc b/tests/test_config_flag_proselintrc new file mode 100644 index 000000000..48f637926 --- /dev/null +++ b/tests/test_config_flag_proselintrc @@ -0,0 +1,85 @@ +{ + "max_errors": 1000, + "checks": { + "airlinese.misc" : true, + "annotations.misc" : true, + "archaism.misc" : true, + "cliches.hell" : true, + "cliches.misc" : true, + "consistency.spacing" : true, + "consistency.spelling" : true, + "corporate_speak.misc" : true, + "cursing.filth" : true, + "cursing.nfl" : false, + "cursing.nword" : true, + "dates_times.am_pm" : true, + "dates_times.dates" : true, + "hedging.misc" : true, + "hyperbole.misc" : true, + "jargon.misc" : true, + "lexical_illusions.misc" : true, + "lgbtq.offensive_terms" : true, + "lgbtq.terms" : true, + "links.broken" : false, + "malapropisms.misc" : true, + "misc.apologizing" : true, + "misc.back_formations" : true, + "misc.bureaucratese" : true, + "misc.but" : true, + "misc.capitalization" : true, + "misc.chatspeak" : true, + "misc.commercialese" : true, + "misc.composition" : true, + "misc.currency" : true, + "misc.debased" : true, + "misc.false_plurals" : true, + "misc.illogic" : true, + "misc.inferior_superior" : true, + "misc.institution_name" : true, + "misc.latin" : true, + "misc.many_a" : true, + "misc.metaconcepts" : true, + "misc.metadiscourse" : true, + "misc.narcissism" : true, + "misc.not_guilty" : true, + "misc.phrasal_adjectives" : true, + "misc.preferred_forms" : true, + "misc.pretension" : true, + "misc.professions" : true, + "misc.punctuation" : true, + "misc.scare_quotes" : true, + "misc.suddenly" : true, + "misc.tense_present" : true, + "misc.waxed" : true, + "misc.whence" : true, + "mixed_metaphors.misc" : true, + "mondegreens.misc" : true, + "needless_variants.misc" : true, + "nonwords.misc" : true, + "oxymorons.misc" : true, + "psychology.misc" : true, + "redundancy.misc" : true, + "redundancy.ras_syndrome" : true, + "skunked_terms.misc" : true, + "spelling.able_atable" : true, + "spelling.able_ible" : true, + "spelling.athletes" : true, + "spelling.em_im_en_in" : true, + "spelling.er_or" : true, + "spelling.in_un" : true, + "spelling.misc" : true, + "security.credit_card" : true, + "security.password" : true, + "sexism.misc" : true, + "terms.animal_adjectives" : true, + "terms.denizen_labels" : true, + "terms.eponymous_adjectives" : true, + "terms.venery" : true, + "typography.diacritical_marks" : true, + "typography.exclamation" : true, + "typography.symbols" : true, + "uncomparables.misc" : false, + "weasel_words.misc" : true, + "weasel_words.very" : true + } +}