New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Settings: Custom parsing of environment variables (non-JSON) #1458
Comments
I think we should emove the That way you could easily override the validator if you so wished. I think this should be backwards compatible so could be done before v2. PR welcome. |
Hi @samuelcolvin, I took a stab at your suggested implementation of moving the json decoding out of the class BaseSettings(BaseModel):
...
@validator('*', pre=True)
def validate_env_vars(cls, v, config, field):
if isinstance(v, str) and field.is_complex():
try:
return config.json_loads(v) # type: ignore
except ValueError as e:
raise SettingsError(f'error parsing JSON for "{field}"') from e
return v Unfortunately, this implementation ran into a problem with two tests: These two tests have an field Moving the json decoding into a validator breaks this functionality because it runs after Option A: Drop support for merging nested objectsOne option would be to drop support for merging nested objects. Complex environment variables seems like an unusual use case to start with, and needing to merge partially specified ones from different sources seems even more unusual. I discussed this with @pjbull and this is what we like the most: it would simplify the code and remove the need for a deep update (a shallow update would be enough). Option B: Merging input streams after the universal decoding validatorCurrently input streams are merged in This doesn't really fit into the existing flow very neatly and would involve making some part of the validation flow more complex. Option C: Keeping decoding in _build_environ and use a different approachSuch as the approaches that @pjbull brainstormed. |
I faced this issue today, as well. I wanted to parse something like this
using such a settings class:
|
Ditto, I'd like to validate a string env var and transform it into a valid submodel. For example, |
Given the complexity of these potential use cases, I'm kind of liking something like proposal (1) in the first comment in this thread. I may have some time on Friday to implement if that approach is interesting |
hi all, taking into account this example, after almost a day of digging I realized that validator is not fired in case the class attribute is of List-type. If attribute type is str, for example, the attribute validator works just fine. I used this code: import os
from typing import List
from pydantic import BaseSettings, validator
os.environ['test_var'] = 'test_val'
class S1(BaseSettings):
test_var: str
@validator('test_var', pre=True)
def val_func(cls, v):
print('this validator is called: {}'.format(v))
return v
class S2(BaseSettings):
test_var: List[str]
@validator('test_var', pre=True)
def val_func(cls, v):
print('this validator is called: {}'.format(v))
return [v] and then instantiating
is there any errors in my example code? |
@zdens per some of the discussion earlier in the thread (kind of mixed in there with proposed changes), the reason you don't see the validator firing is because the code that is failing is the part that parses the environment variables, and that happens before the validators run. Your variable that is the list needs to be valid JSON, like this:
That's why you see a |
@jayqi , |
As a workaround (until something solid is implemented) I changed the following CORS_ORIGINS: List[AnyHttpUrl] = Field(..., env="CORS_ORIGINS") into this: CORS_ORIGINS: Union[str, List[AnyHttpUrl]] = Field(..., env="CORS_ORIGINS") Which, admittedly, is not elegant but enables pydantic to fire up the validator function: @validator("CORS_ORIGINS", pre=True)
def _assemble_cors_origins(cls, cors_origins):
if isinstance(cors_origins, str):
return [item.strip() for item in cors_origins.split(",")]
return cors_origins My # Use comma (,) to separate URLs
CORS_ORIGINS=http://localhost:3000,http://localhost:8000 |
I've just discovered how annoying this can be myself while working on a github action for pydantic 🙈. I'd love someone to fix this, otherwise I will soon. |
🎉 I think that if #1848 goes in then we can check for those types here: And then we can keep the JSON fallback behavior as well. |
I suggest that this issue should also consider adding the ability to parse environment variables into dictionaries directly: import os
from typing import Optional, Dict
from pydantic import BaseSettings, Field, validator
os.environ["COLORS"] = "red:#FF0000,blue:#0000FF,green:#00FF00"
class Example(BaseSettings):
colors: Optional[Dict[str, str]] = Field(None, env="COLORS")
# Result would be:
colors = {"red": "#FF0000", "blue": "#0000FF", "green": "#00FF00"} |
It seems with the addition of I think that now it makes more sense to follow Option 1 described by @pjbull, but also to delegate the whole parsing logic to the Field, i.e:
I could try this implementation if desired @samuelcolvin. |
… via env_parse
… via env_parse
+1, this json stuff for everything that is of non-basic type is somewhat annoying |
… via env_parse
… via env_parse
…se_env_var in Config object (#4406) * Fix #1458 - Allow for custom parsing of environment variables via env_parse * Add docs for env_parse usage * Add changes file for #3977 * fixup: remove stray print statement * Revert env_parse property on field * Add parse_env_var classmethod in nested Config * Update documentation for parse_env_var * Update changes file. * fixup: linting in example * Rebase and remove quotes around imported example * fix example * my suggestions * remove unnecessary Field(env_parse=_parse_custom_dict) Co-authored-by: Samuel Colvin <s@muelcolvin.com>
Settings: Custom parsing of environment variables (non-JSON)
Often we want complex environment variables that are not represented as JSON. One example is a list of items. It's not uncommon to comma-delimit lists like this in bash:
This results in a
JSONDecodeError
. Writing a list of items as valid json is error prone and not human friendly to read in the context of environment variables.Workaround
One workaround is to store the variable as valid json, which is tricky to type correctly in lots of systems where you have to enter environment variables:
Another (simplified) workaround is to set
json_loads
. but its not very elegant sincejson_loads
doesn't know what field its is parsing, which could be error prone:I can see a couple options for implementing the fix:
1. Store the parsing method in the field info extra:
If we take this approach, I think that we can update this logic branch:
https://github.com/samuelcolvin/pydantic/blob/42395056e18dfaa3ef299373374ab3b12bb196ac/pydantic/env_settings.py#L170-L174
Adding something like the following:
2. Add a new config option just for Settings for overriding how env vars are parsed
Another implementation option is to add a new property like
Settings.Config.parse_env_var
which takes thefield
and the value so that it can be overridden to handle dispatching to different parsing methods for different names/properties offield
(currently, just overridingjson_loads
means you are passed a value without knowing where it will be stored so you have to test for and handle all of the possible settings values.Then the following line is the only change that is needed:
https://github.com/samuelcolvin/pydantic/blob/master/pydantic/env_settings.py#L62
Changes to
self.__config__.parse_env_var(field, env_val)
3. Call field validators on the raw string instead of trying to load from json first
Change the same line to:
Pros:
Cons:
4. Custom (de)serialization
Let fields implement custom serialization/deserialization methods. Currently there is
json_encoders
but not an equivalentjson_decoders
for use per-field.There's some discussion of this here: #951
5. Something else
Other ideas? Happy to implement a different suggestion.
Output of
python -c "import pydantic.utils; print(pydantic.utils.version_info())"
:The text was updated successfully, but these errors were encountered: