Skip to content

Commit 6766cff

Browse files
authored
Implements fallback for required tokens (#50)
* Implements fallback value for required tokens * Update docs to include new fallback attribute of Token
1 parent 033fb85 commit 6766cff

File tree

10 files changed

+146
-21
lines changed

10 files changed

+146
-21
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.4.5-beta
5+
---------------------------------------
6+
7+
**Improvements:**
8+
- Adds user values to validate function so names can be validated against expected data.
9+
- Adds fallback to required Tokens, useful for when a Token is required but a default value is needed in case the user doesn't provide one.
10+
411
1.3.1-beta
512
---------------------------------------
613

docs/source/tokens.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@ Tokens are the meaningful parts of a template. A token can be required, meaning
44

55
If options are present, then one of them is the default one. Each option follows a {full_name:abbreviation} schema, so that names can be short but meaning can be recovered easily. The default option might be passed explicitly by the user by passing a *default* argument (it must match one of the options in the Token). If no default options is explicitly passed, the Token will sort options alphabetically and pick the first one. Please notice if you pass the *default* option explicitly, you can use the abbreviation or the full option name.
66

7+
If fallback is defined, it will be used on required tokens if nothing is passed by the user.
8+
79
.. code-block:: python
810
:linenos:
911
10-
n.add_token('whatAffects')
12+
n.add_token('whatLights')
13+
n.add_token('shadowType', fallback='soft')
1114
n.add_token_number('digits')
1215
n.add_token('category', natural='nat',
1316
practical='pra', dramatic='dra',
1417
volumetric='vol', default='nat')
1518
16-
In line 1 we're creating a **Required Token**. This means that for solving the user must provide a value. This is a explicit solve.
19+
In line 1 we're creating a **Required Token**. This means that in order to solve the user must provide a value, else an error will be raised. This is a explicit solve.
20+
21+
In line 2 we're creating a **Required Token** with a fallback. This means that if the user doesn't provide a value, the Token will solve to the fallback value. This is an implicit solve.
1722

18-
In line 2 we're creating a **Number Token**. This is a special Token really useful for working with version like or counting parts of a name. It's always required.
23+
In line 3 we're creating a **Number Token**. This is a special Token really useful for working with version like or counting parts of a name. It's always required.
1924

20-
In line 3 we're creating an **Optional Token**, which means that for solving the user can pass one of the options in the Token or simply ignore passing a value and the Token will solve to it's default option. This is an implicit solve, which helps to greatly reduce the amount of info that needs to be passed to solve for certain cases.
25+
In line 4 we're creating an **Optional Token**, which means that for solving the user can pass one of the options in the Token or simply ignore passing a value and the Token will solve to it's default option. This is an implicit solve, which helps to greatly reduce the amount of info that needs to be passed to solve for certain cases.
2126

2227
For more information on implicit and explicit solving please check :doc:`usage/solving`
2328

docs/source/usage/repositories.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,21 @@ When saving the session, all Tokens and Rules in memory will be saved to the rep
5757
:linenos:
5858
5959
n.add_token('whatAffects')
60+
n.add_token('shadowType', fallback='soft')
6061
n.add_token_number('digits')
6162
n.add_token(
6263
'category',
6364
natural='nat', practical='pra', dramatic='dra',
6465
volumetric='vol', default='nat'
6566
)
6667
67-
In line 1 we're creating a **Required Token**. This means that for solving the user has to provide a value. This is a explicit solve.
68+
In line 1 we're creating a **Required Token**. This means that in order to solve the user must provide a value, else an error will be raised. This is a explicit solve.
6869

69-
In line 2 we're creating a **Number Token**. This is a special Token really useful for working with version like or counting parts of a name. It's always required.
70+
In line 2 we're creating a **Required Token** with a fallback. This means that if the user doesn't provide a value, the Token will solve to the fallback value. This is an implicit solve.
7071

71-
In line 3 we're creating an **Optional Token**, which means that for solving the user can pass one of the options in the Token or simply ignore passing a value and the Token will solve to it's default option. This is an implicit solve, which helps to greatly reduce the amount of info that needs to be passed to solve for certain cases.
72+
In line 3 we're creating a **Number Token**. This is a special Token really useful for working with version like or counting parts of a name. It's always required.
73+
74+
In line 4 we're creating an **Optional Token**, which means that for solving the user can pass one of the options in the Token or simply ignore passing a value and the Token will solve to it's default option. This is an implicit solve, which helps to greatly reduce the amount of info that needs to be passed to solve for certain cases.
7275

7376
For more information on implicit and explicit solving please check :doc:`solving`
7477

docs/source/usage/solving.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Let's set these Tokens and Rules.
1414
1515
# CREATE TOKENS
1616
n.add_token('whatAffects')
17+
n.add_token('shadowType', fallback='soft')
1718
n.add_token_number('digits')
1819
n.add_token(
1920
'category',
@@ -33,7 +34,7 @@ Let's set these Tokens and Rules.
3334
# CREATE RULES
3435
n.add_rule(
3536
'lights',
36-
'{category}_{function}_{whatAffects}_{digits}_{type}'
37+
'{category}_{function}_{whatAffects}_{shadowType}_{digits}_{type}'
3738
)
3839
3940
n.set_active_rule("lights")
@@ -45,6 +46,8 @@ It would not make any sense to make the user pass each and every Token all the t
4546

4647
That's why vfxnaming.solve() accepts both args and kwargs. Not only that, but if given Token is optional and you want to use it's default value, you don't need to pass it at all.
4748

49+
Even if you make a required tokken, you can still define a fallback value for it.
50+
4851
.. code-block:: python
4952
5053
n.solve(
@@ -57,9 +60,9 @@ That's why vfxnaming.solve() accepts both args and kwargs. Not only that, but if
5760
Each of these calls to vfxnaming.solve() will produce the exact same result:
5861

5962
.. note::
60-
natural_custom_chars_001_LGT
63+
natural_custom_chars_soft_001_LGT
6164

62-
If you don't pass a required Token (either as an argument or keyword argument), such as 'whatAffects' in this example, you'll get a **TokenError**. You'll also get a **TokenError** if you try to parse a value that doesn't match any of the options in the Token.
65+
If you don't pass a required Token (either as an argument or keyword argument), such as 'whatAffects' in this example, you'll get a **TokenError**, unless it has a fallback value defined. You'll also get a **TokenError** if you try to parse a value that doesn't match any of the options in the Token.
6366

6467
Solving rules with repeated tokens
6568
-----------------------------------------

docs/source/usage/validating.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Let's set these Tokens and Rule.
2323
2424
# CREATE TOKENS
2525
n.reset_tokens()
26-
n.add_token("whatAffects")
26+
n.add_token("whatAffects", fallback="nothing")
2727
n.add_token_number("digits")
2828
n.add_token(
2929
"category",
@@ -58,6 +58,9 @@ And then let's validate these names:
5858
n.validate("dramatic_bounce_chars_001_LGT")
5959
# Result: True
6060
61+
n.validate("dramatic_bounce_nothing_001_LGT")
62+
# Result: True
63+
6164
n.validate("dramatic_bounce_chars_001")
6265
# Result: False. Last token is missing.
6366

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.4.5-beta"
7+
version = "1.5.0-beta"
88
authors = [
99
{ name="Chris Granados", email="info@chrisgranados.com" },
1010
]

src/vfxnaming/naming.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ def solve(*args, **kwargs) -> AnyStr:
113113
fields_inc += 1
114114
continue
115115
elif token.required and kwargs.get(f) is None and len(args) == 0:
116-
raise SolvingError(f"Token {token.name} is required but was not passed.")
116+
if len(token.fallback):
117+
values[f] = token.fallback
118+
fields_inc += 1
119+
continue
120+
else:
121+
raise SolvingError(
122+
f"Token {token.name} is required but was not passed."
123+
)
117124
# Not required and not passed as keyword argument
118125
elif not token.required and kwargs.get(f) is None:
119126
values[f] = token.solve()
@@ -191,6 +198,11 @@ def validate(name: AnyStr, **kwargs) -> bool:
191198
values[f] = token.solve(kwargs.get(rule.fields[fields_inc]))
192199
fields_inc += 1
193200
continue
201+
elif token.required and isinstance(token, tokens.Token):
202+
if len(token.fallback):
203+
values[f] = token.fallback
204+
fields_inc += 1
205+
continue
194206
fields_inc += 1
195207
logger.debug(f"Validating rule '{rule.name}' with values {values}")
196208
return rule.validate(name, **values)

src/vfxnaming/rules.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ def validate(self, name: AnyStr, **validate_values) -> bool: # noqa: C901
203203
repeated_fields[each] = 1
204204
if repeated_fields:
205205
logger.debug(f"Repeated tokens: {', '.join(repeated_fields.keys())}")
206+
207+
# Validate values passed by the user
206208
if len(validate_values):
207209
for key, value in name_parts:
208210
# Strip number that was added to make group name unique

src/vfxnaming/tokens.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(self, name: AnyStr):
2727
self._name: AnyStr = name
2828
self._default = None
2929
self._options: Dict = {}
30+
self._fallback = ""
3031

3132
def add_option(self, fullname: AnyStr, abbreviation: AnyStr) -> bool:
3233
"""Add an option pair to this Token.
@@ -138,7 +139,9 @@ def solve(self, name: Union[AnyStr, None] = None) -> AnyStr:
138139
"""
139140
if self.required and name:
140141
return name
141-
elif self.required and name is None:
142+
elif self.required and len(self._fallback):
143+
return self._fallback
144+
elif self.required and not name:
142145
raise TokenError(
143146
f"Token {self.name} is required. name parameter must be passed."
144147
)
@@ -227,6 +230,19 @@ def options(self) -> Dict:
227230
"""
228231
return copy.deepcopy(self._options)
229232

233+
@property
234+
def fallback(self) -> AnyStr:
235+
return self._fallback
236+
237+
@fallback.setter
238+
def fallback(self, f: AnyStr):
239+
if self.required:
240+
self._fallback = f
241+
else:
242+
logger.warning(
243+
f"Token '{self.name}' has options, use {self.name}.default instead."
244+
)
245+
230246

231247
class TokenNumber(Serializable):
232248
def __init__(self, name: AnyStr):
@@ -288,7 +304,7 @@ def parse(self, value: AnyStr) -> int:
288304
suffix_index += 1
289305

290306
if prefix_index != -1 and self.prefix != "":
291-
if value[prefix_index : len(self.prefix)] != self.prefix:
307+
if value[prefix_index : len(self.prefix)] != self.prefix: # noqa: E203
292308
logger.warning(f"Prefix '{self.prefix}' not found in '{value}'")
293309
if suffix_index != -1 and self.suffix != "":
294310
if value[-suffix_index:] != self.suffix:
@@ -358,15 +374,18 @@ def options(self) -> Dict:
358374
return copy.deepcopy(self._options)
359375

360376

361-
def add_token(name: AnyStr, **kwargs) -> Token:
377+
def add_token(name: AnyStr, fallback: AnyStr = "", **kwargs) -> Token:
362378
"""Add token to current naming session. If 'default' keyword argument is found,
363379
set it as default for the token instance.
364380
365381
Args:
366382
name (str): Name that best describes the token, this will be used as a way
367383
to invoke the Token object.
368384
369-
kwargs: Each argument following the name is treated as an option for the
385+
fallback (str, optional): Fallback value to use if token is required. Default is ""
386+
and will raise and error, making the token mandatory.
387+
388+
kwargs: Each argument following fallback is treated as an option for the
370389
new Token.
371390
372391
Raises:
@@ -392,6 +411,12 @@ def add_token(name: AnyStr, **kwargs) -> Token:
392411
break
393412
else:
394413
raise TokenError("Default value must match one of the options passed.")
414+
if len(fallback):
415+
if isinstance(fallback, str):
416+
token.fallback = fallback
417+
else:
418+
raise TokenError(f"Fallback must be a string. Got {type(fallback)}")
419+
395420
_tokens[name] = token
396421
return token
397422

tests/tokens_test.py

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ def setup(self):
1313
tokens.reset_tokens()
1414

1515
@pytest.mark.parametrize(
16-
"name,kwargs",
16+
"name,fallback,kwargs",
1717
[
18-
("test", {}),
18+
("test", "", {}),
1919
(
2020
"category",
21+
"",
2122
{
2223
"natural": "natural",
2324
"practical": "practical",
@@ -26,10 +27,11 @@ def setup(self):
2627
"default": "natural",
2728
},
2829
),
30+
("fallbacktest", "imfallback", {}),
2931
],
3032
)
31-
def test_add(self, name: str, kwargs):
32-
result = tokens.add_token(name, **kwargs)
33+
def test_add(self, name: str, fallback: str, kwargs):
34+
result = tokens.add_token(name, fallback, **kwargs)
3335
assert isinstance(result, tokens.Token) is True
3436

3537
def test_reset_tokens(self):
@@ -118,6 +120,69 @@ def test_has_option_abbreviation(self, abbreviation: str, expected: bool):
118120
assert result is expected
119121

120122

123+
class Test_TokenFallback:
124+
@pytest.fixture(autouse=True)
125+
def setup(self):
126+
rules.reset_rules()
127+
tokens.reset_tokens()
128+
tokens.add_token("whatAffects", fallback="nothing")
129+
tokens.add_token_number("number")
130+
tokens.add_token(
131+
"category",
132+
natural="natural",
133+
practical="practical",
134+
dramatic="dramatic",
135+
volumetric="volumetric",
136+
default="natural",
137+
)
138+
tokens.add_token(
139+
"function",
140+
key="key",
141+
fill="fill",
142+
ambient="ambient",
143+
bounce="bounce",
144+
rim="rim",
145+
kick="kick",
146+
custom="custom",
147+
default="custom",
148+
)
149+
tokens.add_token("type", lighting="LGT", default="LGT")
150+
rules.add_rule("lights", "{category}_{function}_{whatAffects}_{number}_{type}")
151+
152+
def test_token_has_fallback(self):
153+
assert tokens.get_token("whatAffects").fallback == "nothing"
154+
155+
@pytest.mark.parametrize(
156+
"name,data,expected",
157+
[
158+
(
159+
"natural_ambient_chars_024_LGT",
160+
{
161+
"category": "natural",
162+
"function": "ambient",
163+
"whatAffects": "chars",
164+
"number": 24,
165+
"type": "lighting",
166+
},
167+
True,
168+
),
169+
(
170+
"natural_ambient_nothing_003_LGT",
171+
{
172+
"category": "natural",
173+
"function": "ambient",
174+
"number": 3,
175+
"type": "lighting",
176+
},
177+
True,
178+
),
179+
],
180+
)
181+
def test_fallback_solve(self, name: str, data: dict, expected: bool):
182+
solved = n.solve(**data)
183+
assert (name == solved) is expected
184+
185+
121186
class Test_TokenNumber:
122187
@pytest.fixture(autouse=True)
123188
def setup(self):

0 commit comments

Comments
 (0)