TemplateClass.py#

Raw Template Class code.

#===========================#
#   CLASS: TEMPLATE CLASS   #
#===========================#
# NOTE: for a full description and line-by-line explanation, see "Scarabaeus Coding
#       Conventions" in the documentation

# NOTE: file name should reflect title above with CamelCase

# TODO: mark any planned functionality, methods, or changes/updates that need to be made for the class
#       here using the TODO identifier. Also note any other TODO's present in methods below 

#-----------#
#  Include  #
#-----------#
import scarabaeus as scb                    # standardized convention for shortening Scarabaeus
from scarabaeus import( Units, ArrayWUnits, # directly import Scarabaeus modules
                       Body)

# some commonly used (but not required) imports
from scarabaeus import Constants            # contains all of the values that should be kept constant across Scarabaeus

import csv                                  # used to read csv files, a common file type used within Scarabaeus
from typing import Literal                  # useful for method inputs, see selector_method_template() for more
from typing import Self                     # used to type-hint a return of the class itself

import numpy as np                          # offers many standard physical constants and math operations
import numpy.typing as npt                  # allows for type-hinting numpy classes like matrix, array-like, etc

#------------------#
#  Generate Units  #
#------------------#
# NOTE: don't include this section if no units are required

# if you know you'll be working with specific units, you can generate them here before the class
km, N = Units.get_units(['km', 'N'])

# you can also generate all of the base units in one line
g, m, sec, rad = Units.get_units('base')

# or create more specific units that you might need
grav_param_unit = km**3*sec**-2

#--------------------#
#  Class Definition  #
#--------------------#
class TemplateClass(Body):
    """
        This class exists as an example to show the format of the object-oriented programming style, format 
        for automatic documentation, and capitalization preferences for titles, properties, and methods 
        in Scarabaeus.

        Remember to leave space between the brief explanation sentence(s) and any more relevant high level information.
        
        The following provides examples for many common formatting use cases. See the Notes section for less 
        common examples, or these references for reStructuredText [1]_ and how to use it in Sphinx [2]_. This 
        cheat sheet [3]_ is also helpful. This is also a good place to see how to reference sources throughout 
        the main docstring using the ``[#]_`` format. 

        .. note :: Use the double back ticks to display as code. Like this :code:```text to display as code```.

        To place warnings and notes, use the ``.. warning:: Some warning`` and ``.. note:: Some note`` 
        directives respectively.

        Bulleted lists can be created using ``*``'s like so:

        * **Bold** text with double asterisks ``**bold text**``.
        * *Italicize* text with single asterisks ``*italic text*``.

        Numbered lists can be created using ``#.`` like so:

        #. The first item in the list
        #. The second item in the list
        #. The third item in the list

        Parameters
        ----------
        arg_one : int | str
            An input to demonstrate a typed parameter with more than one accepted type. 
            Accepted types are:

            * ``int`` - this type works.
            * ``str`` - this type also works.

        arg_two : ArrayWUnits, optional
            An input to demonstrate input validation, in this case ensuring ArrayWUnits 
            have the expected dimensions.
            
            Additionally, to demonstrate optional inputs. Defaults to ``None``.

        arg_three : str, optional
            An input to demonstrate file input and provide an example of private methods 
            (see :func:`__utility_template`). Defaults to ``None``.

        Raises
        ------
        ValueError('Something very bad has happened.')
            | Raised when something very bad happens. Use this section sparingly; if error messages are self-explanatory, you don't need to note them here.

        See Also
        --------
        scarabaeus.ArrayWUnits : Reference similar or important classes to this one.

        Notes
        -----
        The notes section exists to provide additional or more in-depth information pertaining to a class.

        As noted in the summary, this section contains examples for a few less common formatting options. 
        The first is creating tables like this one:

        +---------+---------------+--------+-----------+
        | Planets | Dwarf Planets | Moons  | Asteroids |
        +=========+===============+========+===========+
        | Jupiter | Pluto         | Tethys | Justitia  |
        +---------+---------------+--------+-----------+
        | Saturn  | Haumea        | Phobos | Bennu     |
        +---------+---------------+--------+-----------+

        This table generators like this one [4]_ make creating these tables 
        significantly easier.

        LaTeX syntax can be included like this :math:`r = \\frac{a(1-e^2)}{1+e \\cos\\theta)}`. Or entire 
        equations can also be created like so:

        .. math::

            \\phi = arctan(\\frac{esin\\nu}{1+ecos\\nu})

        You can even include matrices:

        .. math::

            \\frac{\\partial\\mathbf{a}}{\\partial \\mathbf{r}} = 
                \\begin{bmatrix}
                    \\frac{\\partial a_x}{\\partial x} & \\frac{\\partial a_x}{\\partial y} & \\frac{\\partial a_x}{\\partial z} \\\\
                    \\frac{\\partial a_y}{\\partial x} & \\frac{\\partial a_y}{\\partial y} & \\frac{\\partial a_y}{\\partial z} \\\\
                    \\frac{\\partial a_z}{\\partial x} & \\frac{\\partial a_z}{\\partial y} & \\frac{\\partial a_z}{\\partial z}
                \\end{bmatrix}

        Note that double ``\\`` are required for Sphinx to recognize them.

        Quotes can be created using a single indent surrounded by quotation marks:
        
            "Quotes are very nice. - me"

        References
        ----------
        .. [1] https://docutils.sourceforge.io/rst.html
        .. [2] https://sublime-and-sphinx-guide.readthedocs.io/en/latest/topics.html
        .. [3] https://bashtage.github.io/sphinx-material/rst-cheatsheet/rst-cheatsheet.html
        .. [4] https://www.tablesgenerator.com/text_tables

        Examples
        --------
        Provide examples of how to use your class here. Make sure to include the print out 
        as well so that it's easy to see what your code does:

        .. code-block:: python
            
            # initial setup
            import scarabaeus as scb

            # generate units
            kg = scb.Units.get_units('kg')

            # define the example object
            example_input = scb.ArrayWUnits(1, kg)
            example_class = scb.TemplateClass(1, example_input, 'example')

            # show a common use case
            answer = example_class.general_template_method(0)

            >>> print(f'Remember to print the result of the example: {answer}')
            'Remember to print the result of the example: 0'
    """
    #---------------------------#
    # region    Class Constants #
    #---------------------------#
    # NOTE: place class constants before anything else. For more examples, 
    #       see Dimensions.py or Units.py
    cls_const_1 = 1

    cls_const_2 = np.array([[1, 2, 3],
                            [4, 5, 6],
                            [7, 8, 9]])
    
    # endregion Class Constants #
    #---------------------------#

    #--------------------#
    # region Constructor #
    #--------------------#
    def __init__(self, arg_one : int | str, arg_two : ArrayWUnits = None, arg_three : str = None):
        # NOTE: inputs for class methods should be defined with snake_case and given a typing using ":".
        #       This provides information for autocompletion when using the class.

        # NOTE: notice arg_one is typed using a union operator '|' to specify more than one accepted input type.
        #       An example within Scarabaeus of an input like this would be for a SPICE ID input where the
        #       user may give either a numerical (3) or named ('EARTH_BARYCENTER') ID

        # NOTE: if an exception check pertains to input validation, put its logic in the property setter method
        #       for that specific input. See # Properties # section below for more

        # if class is a child of another, perform parent class initializations with super() first
        super().__init__('DEFAULT INPUT', arg_one, arg_two)

        # define the rest of the class properties here
        # NOTE: properties that don't require any more input validation than static typing can be privately defined
        #       with an underscore (ex: self._prop_one = arg_one). However, attributes that we've created a more
        #       complex setter for call that logic by defining it publicly with no underscore (ex: self.prop_two = arg_two).
        #       If it passes the input validation logic in the setter, it will be set privately there
        self._prop_one    = arg_one                             # no extra input validation
        self.prop_two     = arg_two                             # calling this property's setter, no "_"
        self._prop_three  = self.__utility_template(arg_three)  # calling a private utility class, see __utility_template() for more

    #----------------------#
    # region    Properties #
    #----------------------#
    # NOTE: properties serve both to describe components of the class as well as to provide extra input
    #       validation. The "@property" decorator will allow Sphinx to display whatever description you 
    #       provide in the header. This should describe what the property is and its type. For example, a
    #       property "moon_count" in class "Jupiter" would hold information on how many moons Jupiter
    #       has. Prop_one and prop_three are a good examples of this.
    #       Properties can additionaly include a setter to provide extra input validation. See prop_two for 
    #       more on this.

    # NOTE: the properties section can get really crowded and is not super important for actual coding. To help with readability,
    #       the region/endregion delimiters allow it to be collasped with the ">" next to the line number. If you're looking for a class'
    #       properties and don't see them, make sure that the section isn't collapsed first

    # NOTE: properties are inherited from parent classes. You do not need to re-perform any input-validation
    #       for values passed to a child class' parent as those checks will already happen in the parent during super().
    #       You also do not need to re-define the properties again since Sphinx will automatically grab their
    #       descriptions from the parent class.
    @property
    def prop_one(self):
        """
            Some info about this property.

            :base: TemplateClass
            :type: int | str
        """
        return self._prop_one

    # NOTE: create a property setter when input validationis required. This reduces clutter in the __init__file.
    #       Example below: we need to check the units of an ArrayWUnits. First, define the property itself:
    @property 
    def prop_two(self):
        """
            Some info about this property. Expected units of 

            :base: TemplateClass
            :type: ArrayWUnits
        """
        return self._prop_two
    
    # NOTE: now, implement logic (in this case input validation) in the property's setter method
    @prop_two.setter
    def prop_two(self, input_val : ArrayWUnits):
        # let's assume that we expect arg_two to be given in grav_param_units (kg^3/s^2) defined in the # Generate Units # section:
        if isinstance(input_val, ArrayWUnits):
            if input_val.units != grav_param_unit:
                # the given units are incorrect, raise an error
                bad_unit_err = ('Argument [arg_two] should be an ArrayWUnits object with Units of km^3/s^2. '
                                f'Received: {input_val.units}')
                raise ValueError(bad_unit_err)
            
            else:
                # given units are correct, allow property definition
                self._prop_two = input_val

        elif isinstance(input_val, type(None)):
            # in this case, since the default value for arg_two is 'None', an input value of 'None' must also be valid
            self._prop_two = input_val
    
    @property
    def prop_three(self):
        """
            Some info about this property.

            :base: TemplateClass
            :type: str
        """
        return self._prop_three
    
    # NOTE: properties should be defined in the order they are input to the constructor for better in-code readability. Sphinx will
    #       automatically alphabetize them when it builds

    # NOTE: most of the time, properties will have the same name as the constructor input that defines them. In this template
    #       they have separate names that correspond to their inputs to help differentiate between arguments and properties

    # endregion Properties #
    #----------------------#

    # Operator and private methods
    # NOTE: here we are overloading __repr__to allow for more printable reading. Can also define
    #       our own operators here. See ArrayWUnits.py for more examples of operators

    #---------------------#
    # region    Operators #
    #---------------------#
    # NOTE: Because we're overloading __repr__, not creating it, there is no need to display it in Sphinx, so our docstring
    #       should only be a quick note that we're overloading an operator
    def __repr__(self):
        """
            Overloading the printable representation operator for a TemplateClass object.
        """
        return 'TemplateClass object'
    
    # endregion Operators #
    #---------------------#

    #----------------#
    # region Methods #
    #----------------#
    # NOTE: method names should be defined with snake_case

    # NOTE: method order should be private methods -> class/static methods -> instance methods.

    # NOTE: private methods are denoted by the "__" prefix and are intended to be used as utility functions
    #       within the class only. Here we are creating a method to read data in from a given file
    def __utility_template(self, file_name : str) -> np.array:
        """
            Example of a private utility method, denoted by the "__" prefix. In this template, only 
            ``prop_three`` and ``TemplateClass.alt_constr_template`` use this class as a mock file reader, 
            but in practice this could be re-used for as many properties/variables as required 
            (assuming the file-reading protocol is the same between them).

            Parameters
            ----------
            file_name : str
                The name of the file to be read from, expected ``.csv`` or ``.csv-like``.

            Returns
            -------
            data : np.array
                Data read in from the given ``.csv`` filename input.
        """
        # read in data from the input file
        file = open(file_name, "r")
        data = list(csv.reader(file, delimiter = ","))
        file.close()

        # return as a numpy array
        return np.array(data)

    # NOTE: class methods can function like an alternate constructor. They allow us to create instances of the class
    #       using a different set of inputs than specified in the main constructor. As an example, we'll create an instance
    #       of our TemplateClass using a single theoretical file instead of our original inputs.
    @classmethod
    def alt_constr_template(cls, fake_file_name : str) -> Self:
        """
            Creates an instance of TemplateClass given a single theoretical file input.

            Parameters
            ----------
            fake_file_name : tr
                The fake file name we're using for this example.

            Returns
            -------
            class : TemplateClass
                A new instance of ``TemplateClass``.

            Examples
            --------
            Create an instance of ``TemplateClass`` using a file:

            .. code-block:: python

                # intial setup
                import scarabaeus as scb

                # using the normal constructor method
                normal_object = scb.TemplateClass(1, ArrayWUnits(1, None), 'example')

                >>> print(normal_object)
                'TemplateClass made normally`
                
                # using the alternative constructor class method
                alt_constr_object = scb.TemplateClass.alt_constr_template(fake_file_name)

                >>> print(alt_constr_object)
                `TemplateClass made with an alternate constructor`
        """
        # assume that __utility_template reads in data exactly the same for properties and the data needed to create
        # a TemplateClass object
        data = cls.__utility_template(fake_file_name)

        # return an instance of the class with all of its constructor arguments
        return cls(data.arg_one, data.arg_two, data.arg_three)

    # NOTE: instance methods are the most common methods that will be called (and written) in Scarabaeus. Below is a general
    #       template for one
    def general_template_method(self, method_arg : int) -> int:
        """
            Describe the method here.

            Remember that the description should start with a short explanation, we 
            can add extra info below the first sentence.

            Parameters
            ----------
            method_arg : int
                An input to this template method.

            Returns
            -------
            answer : int
                The answer calculated by this template method.

            Notes
            -----
            Remember to read through the guide for correct docstring standards for a 
            more in-depth explanation of writing this kind of documentation.

            Examples
            --------
            You should always include an example of a method if it's any more complicated 
            than a simple value return (which this one is but we'll still include an 
            example as demonstration):

            .. code-block:: python

                # initial setup
                import scarabaeus as scb

                # provide and print the example
                answer = example_class.general_template_method(0)

                >>> print(answer)
                0                
        """
        return method_arg * 1