-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Question: add private attribute #655
Comments
Yes, use underscore or |
This works, but it's not what I want, as every instance will have the same timestamp: class TestExtra(BaseModel):
a: int
_procesed_at: datetime.utcnow() This is what I want, but it fails with the same error: class TestExtra(BaseModel):
a: int
_processed_at: datetime
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._processed_at = datetime.utcnow()
TestExtra(a=1) Or am I missing something? |
@samuelcolvin can this be reopened and solved please? |
This should work: from datetime import datetime
from pydantic import BaseModel
class Test(BaseModel):
a: int
class TestExtra(BaseModel):
a: int
def __init__(self, **kwargs):
super().__init__(**kwargs)
object.__setattr__(self, '_processed_at', datetime.utcnow())
test = {"a": 1}
t1 = Test(**test)
debug(t1)
t2 = TestExtra(**test)
debug(t2)
debug(t2._processed_at) |
(updated with a better solution) If not, you can do something like: from datetime import datetime
from pydantic import BaseModel
class Test(BaseModel):
a: int
class TestExtra(BaseModel):
__slots__ = ('processed_at',)
a: int
def __init__(self, **kwargs):
super().__init__(**kwargs)
object.__setattr__(self, 'processed_at', datetime.utcnow())
t2 = TestExtra(a=123)
debug(t2.dict())
debug(t2.processed_at) outputs:
|
Thanks for the fast answer, Indeed, private
EDIT: I have a In that case, I would like something like that working:
|
Yes, the way we use Using slots is the only solution I know of, but the code is open source, maybe you can find another solution? |
So after digging a bit into both code and doc, I tried with
Then I bypassed the check in the code using:
instead of
Output:
Note that the output is still not what I want: The doc is not very clear about this
What is the scope of model initialization here? I am now looking for a |
|
Please check the PR above, where I implemented this parameter. WDYT ? |
I don't like the idea of having anything other than fields in If we would consider any underscore attrs non-fields, what should we do in this case? from pydantic import BaseModel, Extra
class Model(BaseModel):
class Config:
extra = Extra.allow
print(Model(**{'_a': 'b'})) # _a='b' I think that the best way to keep instance attributes away from fields is to have them in Not only it will be impossible to mix these parameters, but you also would have 30% faster access speed and declared attributes in one place. |
I mostly agree with @MrMrRobat I only grudgingly accepted a solution proposed in #1139 because many people asked for something like this. Perhaps we could have another work around that doesn't require the Something like: from datetime import datetime
from pydantic import BaseModel
class InternalNoOp:
pass
class TestExtra(BaseModel):
__slots__ = ('internal',)
a: int
def __init__(self, **kwargs):
super().__init__(**kwargs)
object.__setattr__(self, 'internal', InternalNoOp())
self.internal.processed_at = datetime.now()
t2 = TestExtra(a=123)
debug(t2.dict())
debug(t2.internal.processed_at) Perhaps we could find a way to hide the setup of |
Is there some way perhaps to use a type decorator of some fashion to annotate internal fields? i.e.
Idea being that such internal fields will be ignored on import.
Why the type annotation then? Because 100% of my work is under mypy strict, and internal fields still need type. I also didn't stipulate that it begins with an underscore: It's fully valid for actual JSON models to use those, so I don't think it's appropriate to enforce namespacing like that. Of course, if the field is "private" by Python convention, it should be available to use, too. @samuelcolvin I'm willing to try to prototype some kind of solution, can you point me to what the limitation with (Filenames and line numbers appreciated if you can provide them.) |
I'm not convinced by
Having thought about this a bit I think the best solution might be #660 which would prevent the field being included in That would allow a property of a model which is a field but is never included in in serialised models. For setting the field, I'm still thinking about computed fields (#935) but they are definitely needed and will fix this. This would completely avoid the need for attributes of a model which are not fields. Until then the best work around is |
I'm not sure that semantic type information is directly an abuse of types, but avoiding their use to limit complexity is still legitimate. I'm not convinced it would be /less/ confusing to users than Whatever the solution, the subject could use a treatment in a heading/section of the docs. A decorator like (Anecdote: I am extremely new to pydantic: I've been trying to prototype with it for about a week. My personal gut reaction is that type annotations in general make good sense, and presumed magic in the base class can be guessed at or reasoned about in a fairly intuitive way. However, I understand little about how type annotations work in conjunction with things like Well, whatever happens, I'll read up on the issues you referenced and see if I can't find a meaningful way to contribute to them, as I'd be very keen in having unlisted/private/internal/excluded/whatever-you-want-to-call-it fields as a first-class feature. (If anyone else wants to dig in, please don't wait for me, but do feel free to tag me on any RFCs if you want testing or comments.) Thanks for this library! |
Maybe a good solution would be not to uses |
I guess, this will require having For instance, removal of |
@maxnoe please let me know if #655 (comment) would solve your problem. If not, please let us know why. |
So we are currently evaluating pydantic for configuration. But in general, our classes also have attributes not related to configuration. Having to use Or am I mistaken about what you imply the solution is in that comment? |
@maxnoe, you can have your own model class MyModel(BaseModel)
def __setattr__(self, attr, value):
if attr in self.__slots__:
object.__setattr__(self, attr, value)
else:
super().__setattr__(self, attr, value) |
Jumping back after a few experiments, here are some thoughts on a higher level:
Here the private attribute _is_registered introduce the possibility to add the logic of verifying if the user is registered (potentially call to database, etc) inside the User object. Rather a
|
@H4dr1en we are evaluating pydantic for a completely different use case. And in this case, we want to be able to support configurable attributes and non-configurable attributes. We are also not expecting a lot of objects, but reading configuration files and command line options, validating them with pydantic and then doing our thing. If this is not the scope of pydantic, we'll have to look for something else, but so far it seemed to work really nice as long as now non-configurable attributes are around. |
@MrMrRobat Ok, so additionally I added a metaclass to inherit the from pydantic import BaseModel
from pydantic.main import ModelMetaclass
import logging
logging.basicConfig(level=logging.INFO)
class InheritSlots(ModelMetaclass):
def __new__(cls, name, bases, namespace):
slots = set(namespace.pop('__slots__', tuple()))
for base in bases:
if hasattr(base, '__slots__'):
slots.update(base.__slots__)
if '__dict__' in slots:
slots.remove('__dict__')
namespace['__slots__'] = tuple(slots)
return ModelMetaclass.__new__(cls, name, bases, namespace)
class Configurable(BaseModel, metaclass=InheritSlots):
__slots__ = ('log', )
def __setattr__(self, attr, value):
if attr in self.__slots__:
object.__setattr__(self, attr, value)
else:
super().__setattr__(attr, value)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.log = logging.getLogger(
self.__class__.__module__ + '.' + self.__class__.__qualname__
)
class Foo(Configurable):
__slots__ = ('test_foo', )
a: int
class Bar(Configurable):
__slots__ = ('test_bar', )
foo: Foo
foo = Foo(a=10)
foo.log.info('Foo')
foo.test_foo = 'test'
bar = Bar(foo={'a': 10})
bar.foo.test_foo = 'test2'
bar.test_bar = 'test3' |
In your example, you are right: you want a manager class. I don't think this is the ONLY reason to want private/non-serialized instance variables, though. Case 1I want to add a computed lookup table that's in a more useful form internally to python than the form it comes in as over the wire. I don't control the API format, and every language is different, so changing the API is not an option. For example, it's a common pattern to send a list of records: In Pydantic, maybe we'd write: class Record(BaseModel):
id: int
record: str
class Query(BaseModel):
record_list: List[Record] We may wish to compute a hash table instead so that the records can be traversed once and retrieved in the future. The class might be extended with some private cache: class Query(BaseModel):
_cache: Dict[int, str]
def _post_load(self):
for record in self.record_list:
self._cache[record.id] = record
def get_record(self, id: int):
return self._cache[id] I wouldn't call this management; it describes a transformation and nothing else. Imagine that the original Pydantic object in question has 20 fields and we want to apply a data transformation to just one of the fields; what happens to the other 19 fields? I'm worried that there's a lot of needless repetition involved there. In general, I wonder how to extend the functionality of a Pydantic object except through "has-a" relationships, which are the right choice for a manager, but the wrong choice for extending functionality. Case 2Let's say we actually ARE adding management code, but what we are managing directly involves the data being manipulated. Let's say we want to add a You could wrap all possible reads/writes in the manager, but again this runs into problems where you have to reflect all of the fields back up into the manager and you wind up with a lot of duplicated code that has to be updated if the model changes. Or if we had a Have I misunderstood something?I think there are fairly legitimate reasons to want state that's considered separate from the canonical model. Using I saw samuelcolvin mention in #655 (comment) that creating an "excluded fields" configuration might be an option, and that'd work just fine, probably. I imagine once we reached that point it wouldn't be a far throw to create a decorator that made it easy to annotate excluded fields. (I'm willing to help, it just seemed like maybe there wasn't a lot of clarity on if this feature was truly needed or even wanted, so I wanted to get that squared away first ...) Maybe I'm not understanding some other workarounds or why this isn't really a problem, or not a valid thing to want to do with pydantic objects, though. |
@jnsnow, seems fair enough. I have a couple ideas about how it can be implemented. Will be happy to make a PR in the near future. |
I have a set of classes that require a non-pedantic managed variable. I'm using If there is not a good way of doing this, one possible design solution might be:
telling pydantic to ignore variables that have been included in It is possible that the pull request by MrMrRobat will allow this code to work (perhaps by renaming Update: I got this to work. Functional, but ugly.
|
@eykamp You can try: from __future__ import annotations
from typing import TYPE_CHECKING
class Device(BaseModel):
id: str
name: str
if TYPE_CHECKING:
token: str = ""
else:
__slots__ = ["token"] |
@eykamp, @nicolas-geniteau I see this as a simpler and cleaner way: class Device(BaseModel):
id: str
name: str
_token: str
__slots__ = ["_token"] |
For some reasons, I thought that slots were not inherited, but the mix of your snipper @MrMrRobat and @maxnoe setter is working for my use case. from typing import Optional
from pydantic import BaseModel
import requests
import logging
import abc
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
class BaseModelWithSlotSetter(BaseModel):
def __setattr__(self, attr, value) -> None:
if attr in self.__slots__:
object.__setattr__(self, attr, value)
else:
super().__setattr__(attr, value)
def _init_slots(self) -> None:
pass
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._init_slots()
class Sensor(BaseModelWithSlotSetter, abc.ABC):
id: str
name: str
_token: Optional[int] # I don't think this is needed, but good for IDE autocomplete
__slots__ = ["_token"]
def _init_slots(self):
# We can't set the default at the class definition level because of __slots__ conflicts with class variable
self._token = None
def send_to_backend(self):
if not self._token:
raise Exception("Need a token")
res = requests.post(f"https://reqbin.com/echo/post/json?token={self._token}", self.json())
return res.json()
def set_token(self, token: int):
self._token = token
class TemperatureDevice(Sensor):
temperature: float
a = TemperatureDevice(id="t1234", name="Temp Device 1234", temperature=28.4)
# No type validation here because we are bypassing Pydantic for the slot setter but Pycharm/Mypy are not happy
a.set_token("asdasddasdasdas")
logging.info(a.send_to_backend())
|
The workaround with slots works great, but when I copy the object, I cannot access the attribute for the copy. Does anyone know why?
If I do the exact same thing with a class that doesn't inherit from BaseModel, it works, so it seems like pydantic is preventing slots to be copied in some way? |
Does it work if you use the more generic python builtin
|
@maxnoe: I have tested with copy.copy, copy.deepcopy and BaseModel.copy(deep=True), and they all give the same error. repl.it here: https://repl.it/@FilipLange/TraumaticCoordinatedChemistry |
Could we reopen this issue? I believe we should be able to use Pydantic simply to enrich a business model with more precise definitions. For instance, instead of: # v1
from datetime import datetime
class Device:
serial_number: str
_creation_date: datetime
def __init__(self, serial_number: str) -> None:
self.serial_number = serial_number
self._creation_date = datetime.now()
def get_age_in_seconds(self) -> float:
return (datetime.now() - self._creation_date).total_seconds() we could have: # v2
import pydantic
from datetime import datetime
class Device(pydantic.BaseModel):
serial_number: str = pydantic.Field(regex=r'^[0-9a-f]{16}$')
_creation_date: datetime
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._creation_date = datetime.now()
def get_age_in_seconds(self) -> float:
return (datetime.now() - self._creation_date).total_seconds() without putting all the validation logic into the constructor or a factory. It seems like a simple need, this workaround works but is very unnatural: # v3
import pydantic
from datetime import datetime
class Device(pydantic.BaseModel):
serial_number: str = pydantic.Field(regex=r'^[0-9a-f]{16}$')
_creation_date: datetime
__slots__ = ('_creation_date',)
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
object.__setattr__(self, '_creation_date', datetime.now())
def get_age_in_seconds(self) -> float:
return (datetime.now() - self._creation_date).total_seconds()
"""
>>> d = Device(serial_number='0123456789abcdef')
>>> d.get_age_in_seconds()
1.4e-05
>>> d.dict()
{'serial_number': '0123456789abcdef'}
""" Does anybody see how we could have the functionality of v3 with a syntax more close to v2? |
Hi @alexpirine from datetime import datetime
from pydantic import BaseModel, Field, PrivateAttr
class Device(BaseModel):
serial_number: str = Field(regex=r'^[0-9a-f]{16}$')
_creation_date: datetime = PrivateAttr(default_factory=datetime.now)
def get_age_in_seconds(self) -> float:
return (datetime.now() - self._creation_date).total_seconds()
d = Device(serial_number='0123456789abcdef')
print(d.get_age_in_seconds()) # 1e-05
print(d.dict() # {'serial_number': '0123456789abcdef'} |
Hi @PrettyWood Excellent, thank you! Indeed, it seems like the P.S. And actually, I saw the documentation about |
…itty and hiding my constants. pydantic/pydantic#655
Can I disallow fields with underscore prefix being passed to Pydantic constructor? |
Adding this here because my team couldn't find it anywhere else. We had an issue where you wanted to have from pydantic import BaseModel, Field, PrivateAttr
class Device(BaseModel):
serial_number: str = Field(regex=r'^[0-9a-f]{16}$')
_deployment: dict = PrivateAttr(default_factory=dict)
def __init__(self, **data):
super().__init__(**data)
self._deployment["foo"] = "bar"
def _iter(self, **data):
"""Override the iterator to also return the private attributes."""
yield from super()._iter(**data)
for private_attr in self.__private_attributes__:
yield private_attr, getattr(self, private_attr)
d = Device(serial_number='0123456789abcdef')
d._deployment[2] = 2
print(d.dict()) Output: {'serial_number': '0123456789abcdef', '_deployment': {'foo': 'bar', 2: 2}} Note, only use this if you really just want underscore attributes that aren't really private. |
I have a use case that I'd to add an attribute when initialising the instance which is not part of the model, thus should not be validated. Is that possible?
Here's a practical example:
The text was updated successfully, but these errors were encountered: