Metadata-Version: 2.1
Name: classx
Version: 1.0.0
Summary: Create a new kind of classes with truly private attributes and methods.
Home-page: https://pypi.org/project/classx/
Author: Khiat Mohammed Abderrezzak
Author-email: khiat.dev@gmail.com
License: MIT
Keywords: python,classes,encapsulation,privacy,strict-privacy,enforced-privacy,private-attributes,private-methods,private-members,access-control,data-hiding,information-hiding,class-generation,dynamic-classes,class-builder,code-generation,python-ast,ast,runtime,metaprogramming,python-internals,advanced-python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE

<p align="center">
  <img src="https://res.cloudinary.com/ork15yjk/image/upload/classx.png" width="450" alt="classx Logo">
</p>

[![PyPI version](https://badge.fury.io/py/classx.svg)](https://badge.fury.io/py/classx)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/classx?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=BRIGHTGREEN&left_text=downloads)](https://pepy.tech/projects/classx)

Python does not provide truly private attributes and methods. Its `__name` convention only uses name mangling, meaning private members can still be accessed from outside the class.

This library introduces a new kind of class designed for genuine encapsulation. It allows you to define truly private attributes and methods that cannot be directly accessed or bypassed from outside the class, providing a stronger form of privacy than Python's built-in classes.

## Installation

You can install `classx` via pip:

```bash
pip install classx
```

## Operations on classes

```python
import classx


# classx type constructor and methods must be separated as functions.

# The best practice in this case when each method and the constructor are
# separated out of one body is to name each method and the constructor
# with the name of the class for example Person and after it the name of
# method or constructor: Person_constructor, Person_some_method_name.


# ----------------- Controllers -----------------


# Private Method
def Person_name_controller(new_name: str) -> str:

    if not isinstance(new_name, str):
        raise TypeError("The name must be text")

    new_name = new_name.strip()

    if not new_name:
        raise ValueError("The name must not be empty")

    if not new_name.isalpha():
        raise ValueError("The name must be a valid text")

    return new_name


# Private Method
def Person_age_controller(new_age: int) -> int:

    if not isinstance(new_age, int):
        raise TypeError("Age must be an integer")

    if new_age < 0 or new_age > 150:
        raise ValueError("Age is irrational")

    return new_age


# --------- Constructor ---------


# Always Public
def Person_constructor(name: str, age: int) -> None:

    # If arguments are the same private attributes don't rewrite them
    # inside the constructor body (auto declared) but if the private
    # attribute is not one of the args you need to declare it inside.

    # Every attribute inside the constructor is private in default
    # and no way to make it public.

    # Also you can write any logic inside the constructor.
    Person_name_controller(name)
    Person_age_controller(age)

    # If no logic needed inside here just write pass not return
    # it will raise a TypeError


# --------- Setters ---------


# Public Method
def Person_set_name(new_name: str) -> None:

    # Here we name the argument 'new_name' not 'name' like the private
    # attribute 'name' declared in the constructor because if we name it
    # 'name' we confuse the compiler and it will raise a SyntaxError
    # because no self here like in traditional classes self.name the
    # attribute and the argument 'name' not the same but here 'name' the
    # private attribute and 'name' the argument are the same in naming and
    # the best practice in all cases here and traditional classes is to
    # write 'new_name' / self.new_name because is a true new name to set
    # from the user even if it's the same old name as value but we set it
    # again that mean new.

    name = Person_name_controller(new_name)


# Public Method
def Person_set_age(new_age: int) -> None:

    # Here we name the argument new_age not age like the private attribute
    # age declared in the constructor because if we name it age we confuse
    # the compiler and it will raise a SyntaxError because no self here
    # like in traditional classes self.age the attribute and the argument
    # age not the same but here age the private attribute and age the
    # argument are the same in naming and the best practice in all cases
    # here and traditional classes is to write new_age / self.new_age
    # because is a true new age to set from the user even if it's the same
    # old age as value but we set it again that mean new.

    age = Person_age_controller(new_age)


# ----- Getters -----


# Public Method
def Person_get_name() -> str:
    return name


# Public Method
def Person_get_age() -> int:
    return age


# ----- Display -----


# Public Method
def Person_display() -> None:
    print(f"Your name is {name} and your age is {age}")


# All classes it will be created inside classx namespaces not here.

# Here we create a new namespace in classx using his engine and point
# to them using a pointer named ns

ns = classx.create_namespace("ns")

# Here we create the Person class inside the same namespace we point
# to them like this you can create many classes with the same name
# in different namespaces without any collision and without removing
# it from the current namespace.

Person = ns.create_class(
    name="Person",
    constructor=Person_constructor,
    public_methods={
        "set_name": Person_set_name,
        "set_age": Person_set_age,
        "get_name": Person_get_name,
        "get_age": Person_get_age,
        "display": Person_display,
    },
    private_methods=[
        Person_name_controller,
        Person_age_controller,
    ],
)


# Entry Point
def main() -> None:

    p1 = Person("ahmed", 24)  # Create a new Person object
    p1.display()

    print("------")

    p1.set_name("mohamed")  # Set a new name.
    p1.set_age(23)  # Set a new age.
    p1.display()

    print("------")

    try:
        p1.set_name("")
    except ValueError as e0:
        print("ValueError handled")  # Because invalid input.

    try:
        p1.set_age(-7)
    except ValueError as e1:
        print("ValueError handled")  # Because invalid input.

    try:
        print(p1.name)
    except AttributeError as e2:
        print("AttributeError handled")  # Because attribute name is private.

    try:
        print(p1.age)
    except AttributeError as e3:
        print("AttributeError handled")  # Because attribute age is private.

    try:
        p1.name = "unknown"
    except AttributeError as e4:
        print("AttributeError handled")  # Because attribute name is private.

    try:
        p1.age = 22
    except AttributeError as e5:
        print("AttributeError handled")  # Because attribute age is private.

    try:
        p1.set_name = None
    except AttributeError as e6:  # Because methods are just attributes.
        print("AttributeError handled")  # You can't assign to public methods

    try:
        p1.Person_name_controller("user")
    except AttributeError as e7:  # Because methods are just attributes.
        print("AttributeError handled")  # Because this method is private.

    try:
        p1.Person_age_controller(7)
    except AttributeError as e8:  # Because methods are just attributes.
        print("AttributeError handled")  # Because this method is private.

    try:
        p1.gender = "male"
    except AttributeError as e9:
        print("AttributeError handled")  # You can't create attributes from outside.


if __name__ == "__main__":
    main()
```

```python
import classx


# Custom Error
class BankError(Exception):
    pass


# Private Method
def BankAccount_owner_name_controller(name: str) -> str:

    if not isinstance(name, str):
        raise TypeError("The name must be text")

    name = name.strip()

    if not name:
        raise ValueError("The name must not be empty")

    if not name.isalpha():
        raise ValueError("The name must be a valid text")

    return name


# Private Method
def BankAccount_amount_controller(amount: int) -> int:

    if not isinstance(amount, (int, float)):
        raise TypeError("The amount must be a number")

    if amount <= 0:
        raise ValueError("The amount must be greater than 0")

    return amount


# Constructor (Always Public)
def BankAccount_constructor(owner: str, balance: int) -> None:

    BankAccount_owner_name_controller(owner)
    BankAccount_amount_controller(balance)

    if balance < 10000:

        raise BankError("You need amount of 10000 or greater to open a new account")


# Public Method
def BankAccount_deposit(amount: int) -> None:

    balance += BankAccount_amount_controller(amount)

    print(f"Deposited {amount}. Current Balance: {balance}")


# Public Method
def BankAccount_withdraw(amount: int) -> None:

    BankAccount_amount_controller(amount)

    if amount > balance:
        raise BankError("Not enough balance")

    balance -= amount
    print(f"Withdrawn {amount}. Current Balance: {balance}")


# Public Method
def BankAccount_display_balance() -> None:

    print(f"Account Holder: {owner}")
    print(f"Balance: {balance}")


ns = classx.create_namespace("ns")

BankAccount = ns.create_class(
    name="BankAccount",
    constructor=BankAccount_constructor,
    public_methods={
        "deposit": BankAccount_deposit,
        "withdraw": BankAccount_withdraw,
        "display_balance": BankAccount_display_balance,
    },
    private_methods=[
        BankAccount_owner_name_controller,
        BankAccount_amount_controller,
    ],
    # You need to mention all used components inside methods and constructor.
    components={BankError.__name__: BankError},
)


# Entry Point
def main() -> None:

    account = BankAccount("mohamed", 10000)

    account.display_balance()

    print("------")

    account.deposit(5000)
    account.withdraw(3000)

    print("------")

    account.display_balance()

    print("------")

    try:
        account.withdraw(50000)
    except BankError as e0:
        print("BankError handled")

    try:
        account.deposit(-10)
    except ValueError as e1:
        print("ValueError handled")


if __name__ == "__main__":
    main()
```

```python
import classx


ns = classx.create_namespace("ns")

Database = ns.create_class(
    name="Database",
    singleton=True,  # To make a singleton class
)


def main() -> None:

    db1 = Database()
    db2 = Database()

    print(id(db1))  # Same id as db2 id.
    print(id(db2))  # Same id as db1 id.

    print(db1 is db2)  # True

    # To check if a class singleton or not.
    print(ns.is_singleton(Database))  # True


if __name__ == "__main__":
    main()
```

```python
import classx


ns = classx.create_namespace("ns")

User = ns.create_class(
    name="User",
    overwrite=True,  # To make the class overwritable
)


def main() -> None:

    global User

    User = ns.create_class(name="User")  # overwrite is False in default.

    # Now you can't overwrite again directly you need to get
    # the permissions from the last overwritable class version.

    try:
        User = ns.create_class(name="User")
    except TypeError as e0:
        # Because the last overwritable class version is not overwritable
        print("TypeError handled")

    # To check if a class is overwritable or not
    print(ns.is_overwritable(User))  # False


if __name__ == "__main__":
    main()
```

```python
import classx


ns = classx.create_namespace("ns")

Image = ns.create_class(
    name="Image",
    public_constants={"format": "PNG"},
)


def main() -> None:

    img = Image()
    print(img.format)  # read only public attribute.


if __name__ == "__main__":
    main()
```

```python
import classx


# In this case we don't write Circle_square or Square_square because is
# used in the two classes is common between them with neutral name and
# also you can use square directly as a normal function.


def square(value: float) -> float:
    return value * value


def Circle_constructor(radius: float) -> None:
    pass


def Circle_area() -> float:
    return 3.14159 * square(radius)


def Square_constructor(side: float) -> None:
    pass


def Square_area() -> float:
    return square(side)


ns = classx.create_namespace("ns")

Circle = ns.create_class(
    name="Circle",
    constructor=Circle_constructor,
    public_methods={
        "area": Circle_area,
    },
    private_methods=[square],
)

Square = ns.create_class(
    name="Square",
    constructor=Square_constructor,
    public_methods={
        "area": Square_area,
    },
    private_methods=[square],
)


def main() -> None:

    ci = Circle(3)
    print(ci.area())

    print("------")

    sq = Square(3)
    print(sq.area())


if __name__ == "__main__":
    main()
```

```python
import classx



ns1 = classx.create_namespace("ns1")
ns2 = classx.create_namespace("ns2")


def main() -> None:

    ns1.set_class(name="User")
    ns2.set_class(name="User")

    print(ns1.contains(ns1.get_class("User")))  # True
    print(ns2.contains(ns2.get_class("User")))  # True

    print(ns1.contains(ns2.get_class("User")))  # False
    print(ns2.contains(ns1.get_class("User")))  # False


if __name__ == "__main__":
    main()
```

```python
import classx


ns = classx.create_namespace("ns")

User = ns.create_class(name="User")


def main() -> None:

    global User

    print(User)  # <function User at @ddress>
    print(User.__name__)  # User

    # Just like JavaScript typeof AnyCls === "function"
    print(type(User))  # User class is function object (meta class function).

    print(type(User).__name__)  # function

    print(type(type(User)))  # function class is type object (super meta class type).
    print(type(type(User)).__name__)  # type

    print(ns.methods())  # Show all namespace methods

    # To check if a class exist inside a namespace or not
    print(ns.exists("User"))  # True

    # or

    # print(ns.exists(User.__name__))  # True

    print(ns.length())  # Show how many classes in this namespace.

    print(ns.classes())  # Show all classes in this namespace.

    # Check if ns contains this User class version or not
    print(ns.contains(User))  # True

    user = User()
    print(user)  # User() object with no attributes/methods (empty).

    print(type(user))  # <class 'classx.User'>
    print(type(user).__name__)  # User

    print(ns.type(user))  # <function User at @ddress>
    print(ns.type(user).__name__)  # User

    print(ns.isinstance(user, User))  # True

    ns.remove_class("User")  # Remove exact class User with name.

    # or

    # ns.remove_class(User.__name__)

    print(ns.classes())  # () zero class

    ns.set_class(name="User")

    print(
        ns.contains(User)
    )  # False (Bceause this class version with this pointer are deleted from ns)

    # Now global User pointer point to the new User class
    User = ns.get_class("User")

    print(ns.contains(User))  # True

    ns.clear_classes()
    print(ns.classes())  # () zero class


if __name__ == "__main__":
    main()
```

```python
import classx
import typing



ns = classx.create_namespace("ns")

User1 = ns.create_class(name="User1")
User2 = ns.create_class(name="User2")


def main() -> None:

    user1 = User1()
    user2 = User2()

    print(ns.isinstance(user1, User1))  # True
    print(ns.isinstance(user2, User2))  # True

    print(ns.isinstance(user1, (User1, user2)))  # True
    print(ns.isinstance(user2, (User1, User2)))  # True

    print(ns.isinstance(user1, typing.Union[User1, User2]))  # True
    print(ns.isinstance(user2, typing.Union[User1, User2]))  # True

    print(ns.isinstance(user1, User2))  # False
    print(ns.isinstance(user2, User1))  # False


if __name__ == "__main__":
    main()
```

```python
import classx


class Node:

    __slots__: tuple[str, str] = ("data", "next")

    def __init__(self, data: object) -> None:

        self.data = data
        self.next: None | Node = None


def LinkedList_constructor() -> None:

    head: None | Node = None
    tail: None | Node = None
    length: int = 0


def LinkedList_add(data: object) -> None:

    node = Node(data)

    if head is None:
        head = node
    else:
        tail.next = node

    tail = node
    length += 1


def LinkedList_length() -> None:
    return length


def LinkedList_show() -> None:

    current: Node = head
    string: list = []

    while current:

        string.append(f"{current.data} -> ")
        current = current.next

    string.append("None")
    print("".join(string))


ns = classx.create_namespace("ns")

LinkedList = ns.create_class(
    name="LinkedList",
    constructor=LinkedList_constructor,
    public_methods={
        "add": LinkedList_add,
        "length": LinkedList_length,
        "show": LinkedList_show,
    },
    # This library intentionally omits inheritance in favor of composition.
    components={Node.__name__: Node},
)


def main() -> None:

    obj = LinkedList()

    obj.show()
    print(f"length: {obj.length()}")

    print("------")

    obj.add(1)
    obj.show()
    print(f"length: {obj.length()}")

    print("------")

    obj.add(2)
    obj.show()
    print(f"length: {obj.length()}")

    print("------")

    obj.add(3)
    obj.show()
    print(f"length: {obj.length()}")


if __name__ == "__main__":
    main()
```

```python
import classx


# Here we named the constructor with the same class name just like c++
# and java constructor style but when we create another pointer with the
# same class name point to the class it's self after creating it we loose
# the original constructor function pointer but it doesn't matter because
# also when we create a normal class we don't have pointers to the methods
# inside it this is the default because we don't need them.

# If you create the class inside the main function you need to use global
# keyword with the constructor function pointer to avoid UnboundLocalError


def Point(x: int, y: int) -> None:
    pass


# We can use private attributes as public attributes if we make setters
# without any validation and the good thing in this case we can make this
# public attributes any time private with adding a validation layer.


def Point_get_x() -> int:
    return x


def Point_set_x(new_x: int) -> None:
    x = new_x


def Point_get_y() -> int:
    return y


def Point_set_y(new_y: int) -> None:
    y = new_y


ns = classx.create_namespace("ns")

Point = ns.create_class(
    name="Point",
    constructor=Point,
    public_methods={
        "get_x": Point_get_x,
        "set_x": Point_set_x,
        "get_y": Point_get_y,
        "set_y": Point_set_y,
    },
)


def main() -> None:

    point = Point(1, 3)

    print(point.get_x())
    print(point.get_y())

    point.set_x(5)
    point.set_y(7)

    print(point.get_x())
    print(point.get_y())


if __name__ == "__main__":
    main()
```

```python
import classx


# You can create public static attributes/methods from outside the class


def greet(name: str) -> str:
    return f"Hello, {name}!"


# You can access public static attributes/methods inside the class methods


def User_get_name() -> str:
    return User.name


def User_set_name(name: str) -> None:
    User.name = name
    print(User.greet(name))


ns = classx.create_namespace("ns")

User = ns.create_class(
    name="User",
    public_methods={
        "get_name": User_get_name,
        "set_name": User_set_name,
    },
)


# Public Static Attribute
User.name = "ahmed"

# Chain the greet function with the User class as public static method.
User.greet = greet


def main() -> None:

    print(User.name)
    print(User.greet(User.name))

    user = User()

    print(user.get_name())
    user.set_name("mohamed")
    print(user.get_name())

    print(User.name)


if __name__ == "__main__":
    main()
```

## Operations on namespaces

```python
import classx as clsx


ns = clsx.create_namespace("ns", overwrite=True)  # set and get a new namespace.


def main() -> None:

    global ns

    print(type(clsx))  # <class 'classx.Engine'>
    print(type(ns))  # <class 'classx.Namespace'>

    print(clsx.is_overwritable(ns))  # True

    clsx.set_namespace("ns")  # set a new namespace (overwrite is False in default).

    ns = clsx.get_namespace("ns")  # get the namespace ns

    print(clsx.is_overwritable(ns))  # False

    print(ns.name())  # "ns"

    print(clsx.exists("ns"))  # True

    print(clsx.is_namespace(ns))  # True

    print(clsx.length())  # 1 namespace

    print(clsx.methods())  # Show all namespaces methods

    print(clsx.namespaces())  # show all current namespaces

    clsx.remove_namespace("ns")  # Remove exact namespace ns with name

    # or

    # clsx.remove_namespace(ns.name())

    print(clsx.length())  # 0 namespace

    clsx.set_namespace("ns")

    print(clsx.length())  # 1 namespace

    clsx.clear_namespaces()  # Remove all namespaces

    print(clsx.length())  # 0 namespace


if __name__ == "__main__":
    main()
```

## Modules level

You can use classx to secure your modules like this

```python
# mymodule.py


import sys
import classx


def hello() -> None:
    print("Hello!")


def add(a: int, b: int) -> int:
    return a + b


class Person:
    def __init__(self, name: str) -> None:
        self.name = name

    def greet(self) -> None:
        print(f"Hi, I'm {self.name}")


def Person_wrapper(name: str) -> Person:
    return Person(name)


ns = classx.create_namespace("ns")

mymodule = ns.create_class(
    name="mymodule",
    public_methods={
        hello.__name__: hello,
        add.__name__: add,
        "Person": Person_wrapper,
    },
    components={Person.__name__: Person},
)


sys.modules[__name__] = mymodule()


def main() -> None:
    print("mymodule")


if __name__ == "__main__":
    main()
```

```python
import mymodule


def main() -> None:

    print(type(mymodule))  # <class 'classx.mymodule'>

    mymodule.hello()

    print(mymodule.add(1, 2))

    person = mymodule.Person("mohamed")
    person.greet()

    try:
        person.greet = "something"
    except AttributeError as e0:
        print("AttributeError handled")  # When you use a classx type

    try:
        person.greet()
    except TypeError as e1:
        print("TypeError handled")  # When you use a normal class

    try:
        mymodule.hello = "something"
    except AttributeError as e2:
        print("AttributeError handled")

    try:
        mymodule.add = "something"
    except AttributeError as e3:
        print("AttributeError handled")

    try:
        mymodule.Person = "something"
    except AttributeError as e4:
        print("AttributeError handled")


if __name__ == "__main__":
    main()
```

The same module example using classx type Person for full privacy and encapsulation

```python
# mymodule.py


import sys
import classx


def hello() -> None:
    print("Hello!")


def add(a: int, b: int) -> int:
    return a + b


def Person_constructor(name: str) -> None:
    pass


def Person_greet() -> None:
    print(f"Hi, I'm {name}")


ns = classx.create_namespace("ns")

Person = ns.create_class(
    name="Person",
    constructor=Person_constructor,
    public_methods={"greet": Person_greet},
)


def Person_wrapper(name: str) -> Person:
    return Person(name)


mymodule = ns.create_class(
    name="mymodule",
    public_methods={
        hello.__name__: hello,
        add.__name__: add,
        "Person": Person_wrapper,
    },
    components={Person.__name__: Person},
)


sys.modules[__name__] = mymodule()


def main() -> None:
    print("mymodule")


if __name__ == "__main__":
    main()
```

Don't write `from classx import *` because is not allowed and it's a bad practice.

## License

This project is licensed under the MIT LICENSE - see the [LICENSE](https://opensource.org/licenses/MIT) for more details.
