Skip to content

Add support for Optional types#8

Open
eykamp wants to merge 1 commit intoramazanpolat:masterfrom
eykamp:optional
Open

Add support for Optional types#8
eykamp wants to merge 1 commit intoramazanpolat:masterfrom
eykamp:optional

Conversation

@eykamp
Copy link
Copy Markdown
Contributor

@eykamp eykamp commented Jul 12, 2020

I like to use Optional when defining my types; this lets you do that. Includes test support.

@ramazanpolat
Copy link
Copy Markdown
Owner

By default, you can assign None to any attribute . So you don't need to annotate them with Union[type]. But if you use any other type than the defined one and the conversion fails, then you get ValueError.

e.g:

class Car(Prodict):
    brand: str
    year: int

# Here we assign `None` to `year` and prodict accepts it:
bimmer = Car(brand='BMW', year=None)
print(bimmer)
# {'brand': 'BMW', 'year': None}`

# Here we are assigning a str to `year` but it is converted to `int` by Prodict:
chevy = Car(brand='Chevrolet', year='2020')
(chevy)
# {'brand': 'Chevrolet', 'year': 2020}
type(chevy.year)
# <class 'int'>

# But assigning a type that is different from annotated type(here 'Millenium' as str as opposed to be an int) and 
# not convertible to defined type generates Value Error:
lambo = Car(brand='Lamborghini', year='Millenium')      # conversion from 'Millenium' to int() throws ValueError.
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 25, in __init__
#     self.set_attributes(**kwargs)
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 205, in set_attributes
#     self.set_attribute(k, v)
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 165, in set_attribute
#     self.update({attr_name: constructor(value)})
# ValueError: invalid literal for int() with base 10: 'Millenium'

So, in the end, you don't need to annotate your attributes with Union[type]. But if you want, you can do it for readability and to help IDE for auto code-completion:

class Car(Prodict):
    brand: str
    year: Union[int]

# let's assign `None` to `year`:
benty =  Car(brand='Bentley', year=None)
print(benty)
# {'brand': 'Bentley', 'year': None}

# assign an int:
linco = Car(brand='Lincoln', year=2020)
print(linco)
{'brand': 'Lincoln', 'year': 2020}

# assign a str that is convertible to int:
audi = Car(brand='Audi', year='2020')
print(audi)
# {'brand': 'Audi', 'year': 2020}
type(audi.year)
# <class 'int'>

# For the last, assign a str that is not convertible to int:
caddy = Car(brand='Cadillac', year='nah')          # conversion from 'nah' to int() throws ValueError which is expected
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 25, in __init__
#     self.set_attributes(**kwargs)
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 205, in set_attributes
#     self.set_attribute(k, v)
#   File "Z:\___DEVEL\py\prodict2\prodict\__init__.py", line 165, in set_attribute
#     self.update({attr_name: constructor(value)})
# ValueError: invalid literal for int() with base 10: 'nah'

So you can use Union[type] without any code change. Maybe I'm missing something here.

@eykamp
Copy link
Copy Markdown
Contributor Author

eykamp commented Jul 12, 2020

I know that in the prodict universe, Optional is, well, optional, but I like to use it where appropriate to document that something may or may not be appearing in my incoming json (though I realize that in some sense everything is optional). I'm using it in my classes for reasons unrelated to prodict, and I think it's reasonable for the library to handle that case (especially since it can do so without impairing any other functionality).

As for Union[int], that is really just int:

>>> from typing import Union
>>> Union[int, float]
typing.Union[int, float]    # Union retained
>>> Union[int]
<class 'int'>               # Union is gone
>>> assert(Union[int] is int)

Optional is shorthand for a Union with None, so not quite the same thing:

>>> from typing import Optional
>>> Optional[int]
typing.Union[int, NoneType]

My PR pulls the int out of that box and lets you use Optional if you want (or not if you don't).

@ramazanpolat
Copy link
Copy Markdown
Owner

I've just realized that I've been replacing Optional with Union in my head all around. My bad, sorry. I'll check this asap.

@ramazanpolat
Copy link
Copy Markdown
Owner

You do realize that in prodict, all attributes behave like Optional[type] right? That means you can assign None to them even if you don't use Optional.

Check this:

class Car(Prodict):
    brand: str
    year: int
    color: Optional[str]

honda = Car(brand='Honda', year=None)
print(honda)
# {'brand': 'Honda', 'year': None}
honda.brand = None
print(honda)
# {'brand': None, 'year': None}
honda.color = None
print(honda)
# {'brand': None, 'year': None, 'color': None}
honda.color = 'White'
print(honda)
# {'brand': None, 'year': None, 'color': 'White'}

Therefore you don't need to annotate them with Optional[type]. On the other hand, if you want to do it, you already can. The above code works without any error. Keep in mind that prodict is not aiming to have a strict schema, it's merely a dict with dot accessible keys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants