pythonpython-3.xpdfpyfpdf

pyFPDF .add_page() function breaking when instantiating from classmethod


The add_page() function errors whenever I try to get any user input in __init__() or a classmethod. It works fine when I don't have any methods to get user input, so I think it's somehow interfering.

AttributeError: 'PDF' object has no attribute 'state'. Did you mean: 'rotate'?

Error

Name: ttt
Traceback (most recent call last):
  File "/workspaces/106404228/shirtificate/shirtificate.py", line 63, in <module>
    pdf = PDF.get_name()
  File "/workspaces/106404228/shirtificate/shirtificate.py", line 44, in get_name
    return cls(name)
  File "/workspaces/106404228/shirtificate/shirtificate.py", line 32, in __init__
    self.add_page(self, format='a4')
  File "/home/ubuntu/.local/lib/python3.10/site-packages/fpdf/fpdf.py", line 813, in add_page
    if self.state == DocumentState.CLOSED:
AttributeError: 'PDF' object has no attribute 'state'. Did you mean: 'rotate'?

Code

from fpdf import FPDF


class PDF(FPDF):

    def __init__(self, name):
        if not name:
            raise ValueError("no name")

        self.name = name
        self.add_page(self, format='a4')

    # header
    def header(self):
        self.image("shirtificate.png")
        self.ln(20)


    @classmethod
    def get_name(cls):
        name = input("Name: ")
        return cls(name)


    @property
    def name(self):
        return self._name


    @name.setter
    def name(self, name):
        if not name:
            raise ValueError("no name")
        self._name = name

pdf = PDF.get_name()
pdf.set_font("helvetica", "B", 16)
pdf.output("shirtificate.pdf")

Solution

  • It's not because of the user input, but it's because you are calling .add_page when the FPDF object is not yet properly instantiated. You can replace the user input with a hardcoded name and you would still get the same error. The classmethod is just making the problem more visible.

    You can see the sequence of what's happening from the Traceback:

      File "/workspaces/106404228/shirtificate/shirtificate.py", line 44, in get_name
        return cls(name)
      File "/workspaces/106404228/shirtificate/shirtificate.py", line 32, in __init__
        self.add_page(self, format='a4')
    

    The return cls(name) code will instantiate the FDPF object, which then calls your __init__, which then calls .add_page. But .add_page is an instance method of the FDPF class. It's expected to be called after the object has been instantiated. In your code, since the __init__ of the parent FPDF class wasn't called, then your object would be missing a .state and all the other attributes of a FPDF object, which leads to the error:

    'PDF' object has no attribute 'state'

    You can check the __init__ method of the FPDF class to see what it does. If you are creating a custom subclass, you typically call the parent's __init__ as part of your subclass __init__. (see Why aren't superclass __init__ methods automatically invoked?).

    The fix is to reorganize your code to something like this:

    from fpdf import FPDF
    
    class PDF(FPDF):
        def __init__(self, name):
            if not name:
                raise ValueError("no name")
    
            # Call the parent FPDF init
            super().__init__()
    
            # Add your custom code after
            self.name = name
     
        ...
    
        @classmethod
        def get_name(cls):
            name = input("Name: ")
            # Create the object
            obj = cls(name)
            # Call FPDF object instance methods
            # after it is created to customize
            obj.add_page(format="A4")
            return obj
    
        ...
    
    pdf = PDF.get_name()
    pdf.set_font("helvetica", "B", 16)
    pdf.output("shirtificate.pdf")
    

    Here, the main changes are:

    1. Calling super().__init__() to call the parent's __init__
    2. Calling .add_page after instantiating an object

    Another recommended change would be renaming that classmethod since it does more than just "getting the name". It should be called something like create_from_name or create_with_prompted_name.