Skip to content

Commit 63c2006

Browse files
authored
Validation against certain rules including strict checking of casing as an option (#51)
* Validate against certain rules. Validation now returns a list of valid rules or an empty list if none found
1 parent 6766cff commit 63c2006

File tree

6 files changed

+143
-71
lines changed

6 files changed

+143
-71
lines changed

docs/source/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
================================
33

4+
1.5.1-beta
5+
---------------------------------------
6+
7+
**Improvements:**
8+
- Adds strict validation option. If strict is True, the name must match exactly the casing the rule.
9+
- If no rule names are passed to with_rules, only the active rule is validated.
10+
411
1.4.5-beta
512
---------------------------------------
613

docs/source/usage/validating.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Many times the only thing we need is to know if a name is valid or not for a giv
1414
- The number of expected separators must match with the rule.
1515
- If tokens have options, the given name must use one of those options.
1616
- If token is a number, validates suffix, prefix and padding.
17+
- If strict is passed as True to the validate function, the name must match exactly the casing the rule.
18+
- If no rule names are passed to with_rules, only the active rule is validated.
1719

1820
Let's set these Tokens and Rule.
1921

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "vfxnaming"
7-
version = "1.5.0-beta"
7+
version = "1.5.1-beta"
88
authors = [
99
{ name="Chris Granados", email="info@chrisgranados.com" },
1010
]

src/vfxnaming/naming.py

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import vfxnaming.rules as rules
2525
import vfxnaming.tokens as tokens
2626
from pathlib import Path
27-
from typing import AnyStr, Dict, Union
27+
from typing import AnyStr, Dict, Union, Iterable
2828

2929
from vfxnaming.logger import logger
3030
from vfxnaming.error import SolvingError, RepoError
@@ -138,9 +138,12 @@ def solve(*args, **kwargs) -> AnyStr:
138138
return rule.solve(**values)
139139

140140

141-
def validate(name: AnyStr, **kwargs) -> bool:
142-
"""Validates a name string against the currently active rule and its
143-
tokens if passed as keyword arguments.
141+
def validate( # noqa: C901
142+
name: AnyStr, with_rules: Iterable[str] = [], strict: bool = False, **kwargs
143+
) -> Iterable[rules.Rule]:
144+
"""Validates a name string against the currently active rule if no rules are passed or
145+
against the list of specific rules passed in with_rules.
146+
It also validates its tokens if passed as keyword arguments.
144147
145148
-For rules with repeated tokens:
146149
@@ -162,50 +165,77 @@ def validate(name: AnyStr, **kwargs) -> bool:
162165
Args:
163166
name (str): Name string e.g.: C_helmet_001_MSH
164167
168+
with_rules (list, optional): List of rule names to validate against. Defaults to [].
169+
170+
strict (bool, optional): If False, it'll try to accept casing mismatches.
171+
165172
kwargs (dict): Keyword arguments with token names and values.
166173
167174
Returns:
168-
bool: True if the name is valid, False otherwise.
175+
list: List of validated rules. Empty list if no rule could be validated.
169176
"""
170-
rule = rules.get_active_rule()
171-
# * This accounts for those cases where a token is used more than once in a rule
172-
repeated_fields = dict()
173-
for each in rule.fields:
174-
if each not in repeated_fields.keys():
175-
if rule.fields.count(each) > 1:
176-
repeated_fields[each] = 1
177-
fields_with_digits = list()
178-
for each in rule.fields:
179-
if each in repeated_fields.keys():
180-
counter = repeated_fields.get(each)
181-
repeated_fields[each] = counter + 1
182-
fields_with_digits.append(f"{each}{counter}")
183-
else:
184-
fields_with_digits.append(each)
185-
values = {}
186-
fields_inc = 0
187-
for f in fields_with_digits:
188-
token = tokens.get_token(rule.fields[fields_inc])
189-
if token:
190-
# Explicitly passed as keyword argument
191-
if kwargs.get(f) is not None:
192-
values[f] = token.solve(kwargs.get(f))
193-
fields_inc += 1
194-
continue
195-
# Explicitly passed as keyword argument without repetitive digits
196-
# Use passed argument for all field repetitions
197-
elif kwargs.get(rule.fields[fields_inc]) is not None:
198-
values[f] = token.solve(kwargs.get(rule.fields[fields_inc]))
199-
fields_inc += 1
200-
continue
201-
elif token.required and isinstance(token, tokens.Token):
202-
if len(token.fallback):
203-
values[f] = token.fallback
177+
previously_active_rule = rules.get_active_rule()
178+
if not len(with_rules):
179+
with_rules = [previously_active_rule.name]
180+
validated: Iterable[rules.Rule] = []
181+
for with_rule in with_rules:
182+
rule = rules.get_rule(with_rule)
183+
if not rule:
184+
logger.warning(f"Rule {with_rule} not found.")
185+
186+
rules.set_active_rule(rule)
187+
# * This accounts for those cases where a token is used more than once in a rule
188+
repeated_fields = dict()
189+
for each in rule.fields:
190+
if each not in repeated_fields.keys():
191+
if rule.fields.count(each) > 1:
192+
repeated_fields[each] = 1
193+
fields_with_digits = list()
194+
for each in rule.fields:
195+
if each in repeated_fields.keys():
196+
counter = repeated_fields.get(each)
197+
repeated_fields[each] = counter + 1
198+
fields_with_digits.append(f"{each}{counter}")
199+
else:
200+
fields_with_digits.append(each)
201+
values = {}
202+
fields_inc = 0
203+
for f in fields_with_digits:
204+
token = tokens.get_token(rule.fields[fields_inc])
205+
if token:
206+
# Explicitly passed as keyword argument
207+
if kwargs.get(f) is not None:
208+
values[f] = token.solve(kwargs.get(f))
204209
fields_inc += 1
205210
continue
206-
fields_inc += 1
207-
logger.debug(f"Validating rule '{rule.name}' with values {values}")
208-
return rule.validate(name, **values)
211+
# Explicitly passed as keyword argument without repetitive digits
212+
# Use passed argument for all field repetitions
213+
elif kwargs.get(rule.fields[fields_inc]) is not None:
214+
values[f] = token.solve(kwargs.get(rule.fields[fields_inc]))
215+
fields_inc += 1
216+
continue
217+
elif token.required and isinstance(token, tokens.Token):
218+
if len(token.fallback):
219+
values[f] = token.fallback
220+
fields_inc += 1
221+
continue
222+
fields_inc += 1
223+
logger.debug(f"Validating rule '{rule.name}' with values {values}")
224+
validation = rule.validate(name, strict, **values)
225+
if validation:
226+
rules.set_active_rule(previously_active_rule)
227+
validated.append(rule)
228+
rules.set_active_rule(previously_active_rule)
229+
if not len(validated):
230+
logger.warning(
231+
f"Could not validate {name} with any of the given "
232+
f"rules {', '.join([rule for rule in with_rules])}."
233+
)
234+
else:
235+
logger.info(
236+
f"Name {name} validated with rules: {', '.join([rule.name for rule in validated])}."
237+
)
238+
return validated
209239

210240

211241
def validate_repo(repo: Path) -> bool:

src/vfxnaming/rules.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def parse(self, name: AnyStr) -> Union[Dict, None]:
160160
f"and rule's pattern '{self._pattern}':'{len(expected_separators)}'."
161161
)
162162

163-
def validate(self, name: AnyStr, **validate_values) -> bool: # noqa: C901
163+
def validate(self, name: AnyStr, strict: bool = False, **validate_values) -> bool: # noqa: C901
164164
"""Validate if given name matches the rule pattern.
165165
166166
Args:
@@ -184,14 +184,10 @@ def validate(self, name: AnyStr, **validate_values) -> bool: # noqa: C901
184184
)
185185
return False
186186

187-
regex = self.__build_regex()
187+
regex = self.__build_regex(strict)
188188
match = regex.search(name)
189189
if not match:
190190
logger.warning(f"Name {name} does not match rule pattern '{self._pattern}'")
191-
if regex.search(name.lower()):
192-
logger.warning(
193-
f"Name {name} has casing mismatches with '{self._pattern}'"
194-
)
195191
return False
196192

197193
match_dict = match.groupdict()
@@ -297,7 +293,7 @@ def validate(self, name: AnyStr, **validate_values) -> bool: # noqa: C901
297293

298294
return matching_options
299295

300-
def __build_regex(self) -> re.Pattern:
296+
def __build_regex(self, strict: bool = False) -> re.Pattern:
301297
# ? Taken from Lucidity by Martin Pengelly-Phillips
302298
# Escape non-placeholder components
303299
expression = re.sub(
@@ -320,7 +316,10 @@ def __build_regex(self) -> re.Pattern:
320316
expression = f"{expression}$"
321317
# Compile expression
322318
try:
323-
compiled = re.compile(expression)
319+
if strict:
320+
compiled = re.compile(expression)
321+
else:
322+
compiled = re.compile(expression, re.IGNORECASE)
324323
except re.error as error:
325324
if any(
326325
[

tests/naming_test.py

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from pathlib import Path
22
import pytest
33
import tempfile
4-
from typing import Dict, List
4+
import types
5+
from typing import Dict, List, Union
56

67
from vfxnaming import naming as n
78
import vfxnaming.rules as rules
@@ -236,60 +237,92 @@ def setup(self):
236237
[
237238
(
238239
"dramatic_bounce_chars_001_LGT",
239-
True,
240+
1,
240241
),
241242
(
242243
"dramatic_bounce_chars_001",
243-
False,
244+
0,
244245
),
245246
(
246247
"whatEver_bounce_chars_001_LGT",
247-
False,
248+
0,
248249
),
249250
(
250251
"dramatic_bounce_chars_01_LGT",
251-
False,
252+
0,
252253
),
253254
(
254255
"dramatic_bounce_chars_v001_LGT",
255-
False,
256+
0,
256257
),
257258
(
258259
"dramatic_bounce_chars_1000_LGT",
259-
True,
260+
1,
260261
),
261262
],
262263
)
263-
def test_valid(self, name: str, expected: bool):
264-
assert n.validate(name) is expected
264+
def test_valid(self, name: str, expected: int):
265+
validated = n.validate(name)
266+
assert len(validated) == expected
265267

266268
@pytest.mark.parametrize(
267269
"name,validate_values,expected",
268270
[
269271
(
270272
"dramatic_bounce_chars_001_LGT",
271273
{"category": "dramatic"},
272-
True,
274+
1,
273275
),
274276
(
275277
"dramatic_bounce_chars_001_LGT",
276278
{"whatAffects": "chars"},
277-
True,
279+
1,
278280
),
279281
(
280282
"dramatic_bounce_chars_001_LGT",
281283
{"category": "practical"},
282-
False,
284+
0,
283285
),
284286
(
285287
"dramatic_bounce_chars_001_LGT",
286288
{"whatAffects": "anything"},
287-
False,
289+
0,
288290
),
289291
],
290292
)
291-
def test_valid_with_tokens(self, name: str, validate_values: dict, expected: bool):
292-
assert n.validate(name, **validate_values) is expected
293+
def test_valid_with_tokens(self, name: str, validate_values: dict, expected: int):
294+
validated = n.validate(name, **validate_values)
295+
assert len(validated) == expected
296+
297+
298+
class Test_ValidateHarcodedValues:
299+
@pytest.fixture(autouse=True)
300+
def setup(self):
301+
tokens.reset_tokens()
302+
rules.reset_rules()
303+
tokens.add_token("side", center="C", left="L", right="R", default="center")
304+
tokens.add_token(
305+
"region",
306+
orbital="ORBI",
307+
parotidmasseter="PAROT",
308+
mental="MENT",
309+
frontal="FRONT",
310+
zygomatic="ZYGO",
311+
retromandibularfossa="RETMAND",
312+
)
313+
rules.add_rule("filename", "{side}-ALWAYS_{side}-This_{side}-{region}")
314+
315+
@pytest.mark.parametrize(
316+
"name,strict,expected",
317+
[
318+
("C-ALWAYS_C-This_C-ORBI", False, 1),
319+
("C-always_C-This_C-ORBI", False, 1),
320+
("C-always_C-this_C-ORBI", True, 0),
321+
],
322+
)
323+
def test_valid_harcoded(self, name: str, strict: bool, expected: int):
324+
validated = n.validate(name, strict=strict)
325+
assert len(validated) == expected
293326

294327

295328
class Test_ValidateWithRepetitions:
@@ -322,28 +355,29 @@ def setup(self):
322355
"side3": "right",
323356
"region3": "zygomatic",
324357
},
325-
True,
358+
1,
326359
),
327360
(
328361
"R-MENT_C-PAROT_L-RETMAND",
329362
{
330363
"side2": "center",
331364
},
332-
True,
365+
1,
333366
),
334367
(
335368
"R-MENT_C-PAROT_L-RETMAND",
336369
{
337370
"side": "center",
338371
},
339-
False,
372+
0,
340373
),
341374
],
342375
)
343376
def test_valid_with_repetitions(
344-
self, name: str, validate_values: dict, expected: bool
377+
self, name: str, validate_values: dict, expected: int
345378
):
346-
assert n.validate(name, **validate_values) is expected
379+
validated = n.validate(name, **validate_values)
380+
assert len(validated) == expected
347381

348382

349383
class Test_RuleWithRepetitions:

0 commit comments

Comments
 (0)