Created
October 12, 2018 16:24
-
-
Save theY4Kman/25861e8acd564fe3a0e4a9f45b54a316 to your computer and use it in GitHub Desktop.
pytest plugin to allow matching class names on word boundaries
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
In pytest.ini, we configure python_classes with wildcard patterns that are | |
checked with fnmatch. This can produce some unintentional effects; for instance, | |
the pattern For* also matches ForbidsAnonymousUsers, which we don't wish. | |
This plugin extends the semantics of fnmatch for python_classes, so a dash '-' | |
in the pattern matches a CamelWords boundary. | |
- For* will match "ForbidsAnonymousUsers" as well as "ForCurrentUser" | |
- For-* will match "ForCurrentUser", but not "ForbidsAnonymousUsers" | |
""" | |
import fnmatch | |
import re | |
import inflection | |
from _pytest.python import PyCollector, Class, Module, Instance | |
def underscore(word, lowercase=True): | |
""" | |
Make an underscored, optionally lowercase form from the expression in the string. | |
Example:: | |
>>> underscore("DeviceType") | |
"device_type" | |
As a rule of thumb you can think of :func:`underscore` as the inverse of | |
:func:`camelize`, though there are cases where that does not hold:: | |
>>> camelize(underscore("IOError")) | |
"IoError" | |
""" | |
word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word) | |
word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word) | |
word = word.replace("-", "_") | |
if lowercase: | |
word = word.lower() | |
return word | |
def preprocess_camel_words(s: str) -> str: | |
"""Adds dashes between camel words, preserving underscores | |
>>> preprocess_camel_words('Forbids_AnonymousUsers') | |
'Forbids_Anonymous-Users' | |
>>> preprocess_camel_words('ForbidsAnonymousUsers') | |
'Forbids-Anonymous-Users' | |
>>> preprocess_camel_words('For8x5x4') | |
'For-8-x-5-x-4' | |
""" | |
# "Forbids_AnonymousUsers" -> "forbids_anonymous-users" | |
s = s.replace('_', ' ') | |
s = underscore(s, lowercase=False) | |
s = re.sub('(\d+)', r'_\1_', s) | |
s = s.replace('__', '_') | |
s = re.sub(' _|_ ', ' ', s) | |
s = s.strip('_') | |
s = inflection.dasherize(s) | |
s = s.replace(' ', '_') | |
return s | |
class CamelWordsSensitiveCollector(PyCollector): | |
def classnamefilter(self, name): | |
preprocessed_name = preprocess_camel_words(name) | |
for pattern in self.config.getini('python_classes'): | |
if name.startswith(pattern): | |
return True | |
# check that name looks like a glob-string before calling fnmatch | |
# because this is called for every name in each collected module, | |
# and fnmatch is somewhat expensive to call | |
elif '*' in pattern or '?' in pattern or '[' in pattern: | |
if fnmatch.fnmatch(preprocessed_name, pattern): | |
return True | |
return False | |
PyCollector.Module = type('Module', (CamelWordsSensitiveCollector, Module), {}) | |
PyCollector.Class = type('Class', (CamelWordsSensitiveCollector, Class), {}) | |
PyCollector.Instance = type('Instance', (CamelWordsSensitiveCollector, Instance), {}) | |
def pytest_pycollect_makemodule(path, parent): | |
return PyCollector.Module(path, parent) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment