Skip to content
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

ClassRowFactory #57

Closed
wants to merge 4 commits into from
Closed

ClassRowFactory #57

wants to merge 4 commits into from

Conversation

ms2892
Copy link

@ms2892 ms2892 commented Jan 11, 2024

Issue number of the reported bug or feature request: #

Describe your changes
A clear and concise description of the changes you have made.

Testing performed
Describe the testing you have performed to ensure that the bug has been addressed, or that the new feature works as planned.

Additional context
Add any other context about your contribution here.

Signed-off-by: ms2892 <msadiq074@gmail.com>
@sarahmonod
Copy link
Contributor

sarahmonod commented Jan 25, 2024

Hi @ms2892 and thanks for this PR. Would you please write a test case for it? Even better would be to also add documentation for it, with code examples.

@sarahmonod sarahmonod linked an issue Jan 25, 2024 that may be closed by this pull request
@ms2892
Copy link
Author

ms2892 commented Jan 25, 2024

Hi @ms2892 and thanks for this PR. Would you please write a test case for it? Even better would be to also add documentation for it, with code examples.

Hi @gusmonod, I would be more than happy to explain what I'm doing here. A detailed discussion of it is described in #42 as I can see that you have linked it with the Issue.

As I can understand from the factories defined in factories.py it is a way to retrieve data into the respective data type. This data type can be in various ways and one of the ways that was described in the issue was to retrieve the data in the form of a class.

This functionality is similar to what has been described in psycopg (https://www.psycopg.org/psycopg3/docs/api/rows.html#psycopg.rows.class_row). Here you pass the reference of the class you wish to get the data as.

In factories.py you accomplish this by passing the reference of a function as eventually you wish to use it like a function in the further calls. To mimic the behaviour of a function and also keep track of the class reference, the class row factory can be declared as a class itself. This way when its initialized you can keep the class reference saved as one of its attributes and use the in built call method to use the particular instance like a function. This was described by an example stated within the issue #42 (comment).

So to use this particular class row factory you can simply call it like the other dictionaries but with a class reference passed alongside it as described in the snippet below.

       >>> @dataclass
       >>> class ABC:
       >>>     x: int
       >>>     y: int
       .......
        >>> conn.row_factory = ClassRowFactory(ABC)
        >>> row = conn.cursor().execute("select 1 as x, 2 as y").fetchone()
        >>> print(row)
        <__main__.ABC object at 0x000001CBE9CC8790>
        >>> print(row.x)
        1 

If you have any more queries do let me know

@sarahmonod
Copy link
Contributor

What happens if the types don't match? Or if there is more/less data returned by the query than is expected by the dataclass?

@ms2892
Copy link
Author

ms2892 commented Jan 25, 2024

What happens if the types don't match? Or if there is more/less data returned by the query than is expected by the dataclass?

from dataclasses import dataclass

@dataclass
class ABC:
    a: int
    b: int 

if __name__=='__main__':
    inp1 = {
        'a':123,
        'b':345
    }

    inp2 = {
        'a': 123123.21392173,
        'b': {
            '123':'123'
        }
    }

    inp3 = {
        'a': 123
    }

    inp4 = {
        'c': 123,
        'd': 345
    }

    try:
        class_exp1 = ABC(**inp1)
        print("OUTPUTS:", class_exp1.a, class_exp1.b)
        print("OUTPUT TYPES:",type(class_exp1.a), type(class_exp1.b),'\n')
        print("INP1 works fine")
    except Exception as e:
        print("INP1 Failed",e)

    try:
        class_exp2 = ABC(**inp2)
        print("OUTPUTS:", class_exp2.a, class_exp2.b)
        print("OUTPUT TYPES:",type(class_exp2.a), type(class_exp2.b),'\n')
        print("INP2 works fine")
    except Exception as e:
        print("INP2 Failed",e)

    try:
        class_exp3 = ABC(**inp3)
        print(class_exp3.a, class_exp3.b)
        print("INP3 works fine")
    except Exception as e:
        print("INP3 Failed",e)

    try:
        class_exp4 = ABC(**inp4)
        print(class_exp4.a, class_exp4.b)
        print("INP4 works fine")
    except Exception as e:
        print("INP4 Failed",e)

So there are 4 cases that I could think of

  1. if the input parameters from the database and the dataclass match perfectly
OUTPUTS: 123 345
OUTPUT TYPES: <class 'int'> <class 'int'> 
INP1 works fine
  1. If the data types are different -> datatype is not enforced.
OUTPUTS: 123123.21392173 {'123': '123'}
OUTPUT TYPES: <class 'float'> <class 'dict'> 
INP2 works fine

To combat this then the data class will have a datatype validator for post_init method. Something like

@dataclass
class ABC:
    a: int
    b: int 

    def __post_init__(self):
        for (name, field_type) in self.__annotations__.items():
            if not isinstance(self.__dict__[name], field_type):
                current_type = type(self.__dict__[name])
                raise TypeError(f"The field `{name}` was assigned by `{current_type}` instead of `{field_type}`")

        print("Check is passed successfully")

This will raise an error INP2 Failed The field 'a' was assigned by '<class 'float'>' instead of '<class 'int'>

3 & 4) If there is a mismatch of arguments it will raise an error in both cases as

INP3 Failed ABC.__init__() missing 1 required positional argument: 'b'
INP4 Failed ABC.__init__() got an unexpected keyword argument 'c' 

@ms2892
Copy link
Author

ms2892 commented Jan 25, 2024

5th Case if there is extra stuff returned by the DB.

        'a': 123,
        'b': 123,
        'c': 456
    }

The error raise in such an input is INP3 Failed ABC.__init__() got an unexpected keyword argument 'c'

Signed-off-by: ms2892 <msadiq074@gmail.com>
Signed-off-by: ms2892 <msadiq074@gmail.com>
Signed-off-by: ms2892 <msadiq074@gmail.com>
@ms2892
Copy link
Author

ms2892 commented Feb 26, 2024

@godlygeek @gusmonod Any updates on the review of this PR?

@godlygeek
Copy link
Contributor

Sorry, this is on my list, but I still need to make some time to think about it.

@godlygeek
Copy link
Contributor

Closing this for now - see the rationale in #42 (comment).

I am willing to resurrect this in the future if we change our dbapi2.Connection and dbapi2.Cursor and cdb2.Handle types to be generic so that that static type analysis tools gain an understanding of row factories.

@godlygeek godlygeek closed this Apr 19, 2024
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.

Provide a "class" row factory
3 participants