From 9ed1f5ff6faf194f4b362d028dcea953db49b573 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 17 Aug 2023 21:21:08 -0400 Subject: [PATCH 1/3] TST: Test no-file for source --- numpydoc/tests/test_validate.py | 27 +++++++++++++++++++++++++++ numpydoc/validate.py | 14 ++++++++++---- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/numpydoc/tests/test_validate.py b/numpydoc/tests/test_validate.py index 0191e82a..d50db264 100644 --- a/numpydoc/tests/test_validate.py +++ b/numpydoc/tests/test_validate.py @@ -41,6 +41,33 @@ def test_get_validation_checks_validity(checks): _ = validate.get_validation_checks(checks) +class _DummyList(list): + """Dummy list class to test validation.""" + + +def test_no_file(): + """Test that validation can be done on functions made on the fly.""" + # Just a smoke test for now, will have a None filename + validate.validate("numpydoc.tests.test_validate._DummyList.clear") + # This does something like decorator.FunctionMaker.make + src = """\ +def func(): + '''Do something.''' + return 1 +""" + evaldict = {} + exec(compile(src, "", "single"), evaldict) + func = evaldict["func"] + func.__source__ = src + func.__module__ = "numpydoc.tests.test_validate" + assert func() == 1 + # This should get past the comment reading at least. A properly + # wrapped function *would* have source code, too. We could add such + # a test later + with pytest.raises(OSError, match="could not get source code"): + validate.validate(validate.get_doc_object(func)) + + @pytest.mark.parametrize( ["file_contents", "expected"], [ diff --git a/numpydoc/validate.py b/numpydoc/validate.py index 481b309e..ec29111e 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -7,7 +7,7 @@ """ from copy import deepcopy -from typing import Dict, List, Set +from typing import Dict, List, Set, Optional import ast import collections import importlib @@ -111,7 +111,9 @@ IGNORE_COMMENT_PATTERN = re.compile("(?:.* numpydoc ignore[=|:] ?)(.+)") -def extract_ignore_validation_comments(filepath: os.PathLike) -> Dict[int, List[str]]: +def extract_ignore_validation_comments( + filepath: Optional[os.PathLike], +) -> Dict[int, List[str]]: """ Extract inline comments indicating certain validation checks should be ignored. @@ -125,8 +127,12 @@ def extract_ignore_validation_comments(filepath: os.PathLike) -> Dict[int, List[ dict[int, list[str]] Mapping of line number to a list of checks to ignore. """ - with open(filepath) as file: - numpydoc_ignore_comments = {} + numpydoc_ignore_comments = {} + try: + file = open(filepath) + except (OSError, TypeError): # can be None, nonexistent, or unreadable + return numpydoc_ignore_comments + with file: last_declaration = 1 declarations = ["def", "class"] for token in tokenize.generate_tokens(file.readline): From f18e7807cb3470efe951b595ad2bc730e22a94e9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 18 Aug 2023 07:18:57 -0400 Subject: [PATCH 2/3] ENH: Speed up and simplify --- numpydoc/tests/test_validate.py | 17 ----------------- numpydoc/validate.py | 7 +++++++ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/numpydoc/tests/test_validate.py b/numpydoc/tests/test_validate.py index d50db264..e09c4414 100644 --- a/numpydoc/tests/test_validate.py +++ b/numpydoc/tests/test_validate.py @@ -49,23 +49,6 @@ def test_no_file(): """Test that validation can be done on functions made on the fly.""" # Just a smoke test for now, will have a None filename validate.validate("numpydoc.tests.test_validate._DummyList.clear") - # This does something like decorator.FunctionMaker.make - src = """\ -def func(): - '''Do something.''' - return 1 -""" - evaldict = {} - exec(compile(src, "", "single"), evaldict) - func = evaldict["func"] - func.__source__ = src - func.__module__ = "numpydoc.tests.test_validate" - assert func() == 1 - # This should get past the comment reading at least. A properly - # wrapped function *would* have source code, too. We could add such - # a test later - with pytest.raises(OSError, match="could not get source code"): - validate.validate(validate.get_doc_object(func)) @pytest.mark.parametrize( diff --git a/numpydoc/validate.py b/numpydoc/validate.py index ec29111e..922f817f 100644 --- a/numpydoc/validate.py +++ b/numpydoc/validate.py @@ -10,6 +10,7 @@ from typing import Dict, List, Set, Optional import ast import collections +import functools import importlib import inspect import os @@ -111,6 +112,12 @@ IGNORE_COMMENT_PATTERN = re.compile("(?:.* numpydoc ignore[=|:] ?)(.+)") +# This function gets called once per function/method to be validated. +# We have to balance memory usage with performance here. It shouldn't be too +# bad to store these `dict`s (they should be rare), but to be safe let's keep +# the limit low-ish. This was set by looking at scipy, numpy, matplotlib, +# and pandas and they had between ~500 and ~1300 .py files as of 2023-08-16. +@functools.lru_cache(maxsize=2000) def extract_ignore_validation_comments( filepath: Optional[os.PathLike], ) -> Dict[int, List[str]]: From 12e59fd233f43c191b21c29d93d84c010bf17ea5 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 18 Aug 2023 09:04:50 -0400 Subject: [PATCH 3/3] FIX: Events --- .github/workflows/label-check.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/label-check.yml b/.github/workflows/label-check.yml index a0e2e5c0..5c4aee62 100644 --- a/.github/workflows/label-check.yml +++ b/.github/workflows/label-check.yml @@ -4,8 +4,10 @@ on: pull_request: types: - opened + - repoened - labeled - unlabeled + - synchronize env: LABELS: ${{ join( github.event.pull_request.labels.*.name, ' ' ) }}