25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.
4 |
5 | ## Type of change
6 |
7 | - [ ] Bug fix
8 | - [ ] New feature
9 | - [ ] Breaking change
10 | - [ ] Documentation update
11 | - [ ] Other (describe):
12 |
13 | ## Checklist
14 |
17 | - [ ] My code follows the style guidelines of this project.
18 | - [ ] I tested the code.
19 | - [ ] I have performed a self-review of my own code.
20 | - [ ] I have commented my code, particularly in hard-to-understand areas.
21 | - [ ] I have updated documentation as needed.
22 | - [ ] My changes generate no new warnings/errors.
23 | - [ ] I have added tests that prove my fix is effective or that my feature works.
24 | - [ ] New and existing unit tests pass locally.
25 |
26 | ## Additional context
27 |
28 | Add any other context or screenshots about the pull request here.
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 All PyPositron contributors
4 | Copyright (c) 2025 Tanjim Kamal
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "py_positron"
3 | version = "0.0.2.3"
4 | authors = [
5 | { name="itzmetanjim", email="tanjimkamal1@gmail.com" },
6 | ]
7 | dependencies = [
8 | "pywebview",
9 | "pathlib2",
10 | "packaging"
11 | ]
12 | description = "PyPositron is a python framework for building apps with HTML, CSS and Python. itzmetanjim.github.io/py-positron/"
13 | readme = "README.md"
14 | requires-python = ">=3.9"
15 | classifiers = [
16 | "Development Status :: 3 - Alpha",
17 | "Intended Audience :: Developers",
18 | "Programming Language :: Python :: 3",
19 | "Operating System :: Microsoft :: Windows",
20 | "Operating System :: MacOS",
21 | "Operating System :: POSIX",
22 | "Operating System :: Unix",
23 | "Programming Language :: Python :: 3.10",
24 | "Programming Language :: Python :: 3.11",
25 | "Programming Language :: Python :: 3.12",
26 | "Programming Language :: Python :: 3.13"
27 | ]
28 | license = "MIT"
29 | license-files = ["LICEN[CS]E*"]
30 | keywords = ["python","framework","webview","gui","desktop","cross-platform","electron","pywebview","positron","py-positron","html","css","js","javascript","web","app"]
31 | [project.urls]
32 | Homepage = "https://github.com/itzmetanjim/py-positron"
33 | Issues = "https://github.com/itzmetanjim/py-positron/issues"
34 | Documentation= "https://github.com/itzmetanjim/py-positron/wiki"
35 | Repository= "https://github.com/itzmetanjim/py-positron"
36 |
37 | [build-system]
38 | requires = ["setuptools >= 77.0.3"]
39 | build-backend = "setuptools.build_meta"
40 | [project.scripts]
41 | positron = "py_positron.cli:main"
42 | [tool.setuptools]
43 | [tool.setuptools.packages.find]
44 | where = ["src"]
45 |
46 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to py-positron
2 |
3 | Thank you for considering contributing to **py-positron**\! Your help is greatly appreciated.
4 | Please follow these guidelines to ensure a smooth contribution process.
5 | > Note: If you want to contribute to the website [pypositron.github.io](https://pypositron.github.io), go to [the repo for the website](https://github.com/PyPositron/pypositron.github.io) instead. You can follow the same guidelines, however.
6 | ## Getting Started
7 |
8 | 1. **Fork** the repository.
9 | 2. **Clone** your fork:
10 | ```
11 | git clone https://github.com/your-username/py-positron.git
12 | ```
13 | 3. **Create** a new branch for your changes:
14 | ```
15 | git checkout -b my-feature-branch
16 | ```
17 | ## Things to contribute
18 | * [ ] Make documentation, README and CONTRIBUTING.md that is not AI-generated
19 | * [ ] Add more examples and tutorials
20 | * [ ] Make the code neater
21 | * [ ] Make the installer/executable creation system
22 | * [ ] Test on Linux
23 | * [ ] Add support for MacOS
24 | * [ ] Add building and packaging features (like converting to executables/installers that can run on any system without Python installed)
25 | * [ ] Optimize performance.
26 |
27 | ## Development Guidelines
28 |
29 | - Write clear, concise commit messages.
30 | - Follow [PEP8](https://pep8.org/) for Python code style.
31 | - Keep pull requests focused and small.
32 | - If your change affects the public API, please update the documentation.
33 |
34 | ## Reporting Bugs
35 |
36 | If you find a bug:
37 |
38 | - Open an [issue](https://github.com/itzmetanjim/py-positron/issues) with a clear title and description.
39 | - Include steps to reproduce, expected behavior, and actual behavior.
40 | - Add relevant logs or screenshots if possible.
41 |
42 | ## Submitting a Pull Request
43 |
44 | 1. Ensure your branch is up-to-date with `main`:
45 | ```
46 | git fetch origin
47 | git rebase origin/main
48 | ```
49 | 2. Run tests if applicable.
50 | 3. Open a pull request with a clear description of your changes.
51 |
52 | ## Code of Conduct
53 |
54 | Please be respectful. By participating, you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md).
55 |
56 | ## Thank you
57 |
58 | Your contributions make **py-positron** better\!
59 |
--------------------------------------------------------------------------------
/src/py_positron/activate.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import subprocess
4 | import sys
5 | from pathlib import Path
6 |
7 |
8 | def activate() -> None:
9 | """Activate PyPositron virtual environment."""
10 | os.chdir(os.path.dirname(os.path.abspath("config.json")))
11 | config: dict[str, str] = load_config()
12 |
13 | # switch CWD to project root so all relative paths (entry_file, venvs) resolve correctly
14 | if Path(config["winvenv_executable"]).exists():
15 | windows_activation(config["winvenv_executable"])
16 | elif Path(config["linuxvenv"]).exists():
17 | linux_activation()
18 | else:
19 | exit_activation()
20 |
21 |
22 | def windows_activation(env_path: str) -> None:
23 | """Activate the Windows virtual environment."""
24 | ps1_path: str = str(Path.cwd() / Path(env_path).parent / "activate.ps1")
25 | cmd: list[str] = [
26 | "powershell",
27 | "-NoProfile",
28 | "-ExecutionPolicy",
29 | "Bypass",
30 | "-Command",
31 | f"& '{ps1_path}'",
32 | ]
33 | result = subprocess.run(cmd, check=True)
34 | if result.returncode != 0:
35 | print("Error:", result.stderr, file=sys.stderr)
36 | sys.exit(result.returncode)
37 | sys.exit(0)
38 |
39 |
40 | def linux_activation() -> None:
41 | """Activate the Linux virtual environment."""
42 | cmd: str = 'bash -c "source linuxvenv/bin/activate"'
43 | os.system(cmd)
44 |
45 |
46 | def exit_activation() -> None:
47 | """Exit the virtual environment setup."""
48 | print(
49 | "This project does not contain a PyPositron venv. "
50 | "Please create a new venv with PyPositron venv.",
51 | )
52 | sys.exit(1)
53 |
54 |
55 | def load_config() -> dict[str, str]:
56 | """Load the configuration from config.json."""
57 | config_file: str = "config.json"
58 | if not Path(config_file).exists():
59 | print(
60 | "This folder does not contain a PyPositron project. "
61 | "Please navigate to the project root, where config.json is located."
62 | "\nYou can create a new project with PyPositron create.",
63 | )
64 | sys.exit(1)
65 |
66 | with Path.open(Path(config_file).resolve()) as f:
67 | return json.load(f)
68 |
--------------------------------------------------------------------------------
/src/py_positron/createvenv.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import json
4 | import platform
5 | import subprocess
6 | from pathlib2 import Path
7 | def venv():
8 | if not os.path.exists("config.json"):
9 | print("No config.json found. Please create a project first, or navigate to the project root directory.")
10 | sys.exit(1)
11 | with open(os.path.abspath("config.json"), "r") as f:
12 | config = json.load(f)
13 | if platform.system().lower().strip() == "windows":
14 | config["has_venv"] = True
15 | config["winvenv_executable"] = "winvenv/Scripts/python.exe"
16 | print("Creating venv and installing Positron (this may take a while)...")
17 | ps1_path = Path(__file__).parent / "winvenv_setup.ps1"
18 | absolute = ps1_path.resolve()
19 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}'"]
20 | result = subprocess.run(cmd, capture_output=True, text=True)
21 | if result.returncode != 0:
22 | print("Error:", result.stderr, file=sys.stderr)
23 | print("Failed to create venv. Please create a venv manually and install Positron in it.")
24 | sys.exit(result.returncode)
25 | print("Positron successfully installed in venv.")
26 |
27 | elif platform.system().lower().strip() == "linux":
28 | config["has_venv"] = True
29 | config["linuxvenv"] = os.path.join(os.getcwd(), "linuxvenv")
30 | print("Creating venv (this may take a while)...")
31 | os.system("python3 -m venv ./linuxvenv/")
32 | print("Activating venv...")
33 | os.system("bash -c \"source linuxvenv/bin/activate\"")
34 | print("Installing Positron in venv- if this fails, use pip to install it...")
35 | os.system('bash -c "source linuxvenv/bin/activate && pip3 install -U pip"')
36 | os.system('bash -c "source linuxvenv/bin/activate && pip3 install -U py_positron"')
37 | print("Positron installed in venv.")
38 | elif platform.system().lower().strip() == "darwin":
39 | print("Venv creation is not supported on MacOS through the project creation wizard.\nSee https://github.com/itzmetanjim/py-positron/wiki/Manually-creating-a-venv.")
40 | else:
41 | print("""Could not create venv- Unsupported OS. Please create a venv manually and install Positron in it. See https://github.com/itzmetanjim/py-positron/wiki/Manually-creating-a-venv.""")
42 | sys.exit(1)
43 | print("Updating config.json...")
44 | with open(os.path.abspath("config.json"), "w") as f:
45 | json.dump(config, f, indent=4)
46 | print("Venv created successfully.")
47 | print("You can now install libraries in the venv using positron install .")
--------------------------------------------------------------------------------
/src/py_positron/install.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import argparse
4 | import json
5 | import platform
6 | import subprocess
7 | from pathlib2 import Path
8 | def install(args):
9 | #pip installs a library
10 | parser=argparse.ArgumentParser()
11 | parser.add_argument("library", help="Library to install", type=str)
12 | parser.add_argument("--root_path","-r", help="Root path of the project", type=str,default="--",required=False,action="store")
13 | args=parser.parse_args(args)
14 | directory=args.root_path
15 | if args.root_path=="--":
16 | directory=os.getcwd()
17 | os.chdir(directory)
18 | if not os.path.exists(os.path.join(directory, "config.json")):
19 | print("It seems like the directory you are running this from is not a Positron project.")
20 | print("Please run this command from the root of your Positron project (where config.json is located).")
21 | print("To make a new positron project, run positron create.")
22 | exit(0)
23 | if not os.path.exists(os.path.join(directory,"winvenv", "Scripts")) and not os.path.exists(os.path.join(directory,"linuxvenv", "bin")):
24 | print("It seems like the project you are running this from does not have a venv..")
25 | print("To install a venv in Windows or Linux, run positron venv")
26 | exit(0)
27 | if platform.system().lower().strip() == "windows":
28 | if os.path.exists(os.path.join(directory,'winvenv', "Scripts")):
29 | ps1_path = Path(__file__).parent / "winpackage_installer.ps1"
30 | absolute = ps1_path.resolve()
31 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}' {args.library} \'{directory}\'"]
32 | result = subprocess.run(cmd,check=True)
33 | if result.returncode != 0:
34 | print("Error:", result.stderr, file=sys.stderr)
35 | sys.exit(result.returncode)
36 | else:
37 | print("This project does not have a Windows venv, create one with positron venv")
38 | print("Note that you will have to install all extra libraries again in the Windows venv.")
39 | exit(0)
40 | elif platform.system().lower().strip() == "linux":
41 | if os.path.exists(os.path.join(directory,'linuxvenv', "bin")):
42 | os.system(f"bash -c \"source \'{os.path.join(directory,'linuxvenv', 'bin', 'activate')}\' && pip3 install {args.library}\"")
43 | elif os.path.exists(os.path.join(directory,'winvenv', "Scripts")):
44 | print("This project does not have a Linux venv, create one with positron venv")
45 | print("Note that you will have to install all extra libraries again in the Linux venv.")
46 | exit(0)
47 | elif platform.system().lower().strip() == "darwin":
48 | print("MacOS is not supported yet.")
49 | exit()
50 |
--------------------------------------------------------------------------------
/src/py_positron/updatecmd.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import argparse
4 | import json
5 | import platform
6 | import subprocess
7 | from pathlib2 import Path
8 | def update(args):
9 |
10 | parser=argparse.ArgumentParser(args)
11 | parser.add_argument("library", help="Name of the library to update. This is the same as the name you would use with pip.", type=str,default="",nargs="?")
12 | parser.add_argument("--root_path","-r", help="Root path of the project", type=str,default="--",required=False,action="store")
13 | args=parser.parse_args(args)
14 | library=args.library
15 | if library=="":
16 | library="py-positron"
17 | directory=args.root_path
18 | if args.root_path=="--":
19 | directory=os.getcwd()
20 | os.chdir(directory)
21 | if not os.path.exists(os.path.join(directory, "config.json")):
22 | print("It seems like the directory you are running this from is not a Positron project.")
23 | print("Please run this command from the root of your Positron project (where config.json is located).")
24 | print("To make a new positron project, run positron create.")
25 | exit(0)
26 | if not os.path.exists(os.path.join(directory,"winvenv", "Scripts")) and not os.path.exists(os.path.join(directory,"linuxvenv", "bin")):
27 | print("It seems like the project you are running this from does not have a venv.")
28 | print("This command is not needed if you don't use a venv.")
29 | print("To install a venv in Windows or Linux, run positron venv")
30 | exit(0)
31 | if platform.system().lower().strip() == "windows":
32 | if os.path.exists(os.path.join(directory,'winvenv', "Scripts")):
33 | ps1_path = Path(__file__).parent / "winpackage_installer.ps1"
34 | absolute = ps1_path.resolve()
35 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}' {library} \'{directory}\' 1"]
36 | result = subprocess.run(cmd,check=True)
37 | if result.returncode != 0:
38 | print("Error:", result.stderr, file=sys.stderr)
39 | sys.exit(result.returncode)
40 | else:
41 | print("This project does not have a Windows venv, create one with positron venv")
42 | print("Note that you will have to install all extra libraries again in the Windows venv.")
43 | exit(0)
44 | elif platform.system().lower().strip() == "linux":
45 | if os.path.exists(os.path.join(directory,'linuxvenv', "bin")):
46 | os.system(f"bash -c \"source \'{os.path.join(directory,'linuxvenv', 'bin', 'activate')}\' && pip3 install {library}\"")
47 | elif os.path.exists(os.path.join(directory,'winvenv', "Scripts")):
48 | print("This project does not have a Linux venv, create one with positron venv")
49 | print("Note that you will have to install all extra libraries again in the Linux venv.")
50 | exit(0)
51 | elif platform.system().lower().strip() == "darwin":
52 | print("MacOS is not supported yet.")
53 | exit()
54 | if library=="py-positron":
55 | with open(os.path.join(directory, "config.json"), "r") as f:
56 | config = json.load(f)
57 | config["positron_version"] = "latest"
58 | with open(os.path.join(directory, "config.json"), "w") as f:
59 | json.dump(config, f, indent=4)
60 |
--------------------------------------------------------------------------------
/src/py_positron/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Python-Powered App
5 |
82 |
83 |
84 |
Congratulations!
85 |
You have successfully created your first app using PyPositron!
Open frontend/index.html and backend/main.py and check out the website code.
92 |
93 |
94 |
95 | #You can write small amounts of Python code here for convenience.
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | this repo.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/src/py_positron/cli.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import argparse
3 | import sys
4 | import os
5 | def main():
6 | parser=argparse.ArgumentParser()
7 | parser.add_argument("command", help="Command to run. Available commands: help, create, start, install, pip, activate, venv, update", type=str,choices=["help","create","start","install","activate","venv","pip","update"],default="help",nargs="?")
8 | parser.add_argument("-v","--version", help="Show the version of PyPositron.", action="store_true")
9 | parser.add_argument("nargs",nargs=argparse.REMAINDER,help="Arguments for the command. Type 'positron -h' for more information on the command.")
10 | args=parser.parse_args()
11 | argv=args.nargs
12 |
13 | # Check if help is requested for a specific command
14 | if args.command and ("-h" in argv or "--help" in argv):
15 | # Create sub-parsers with proper program names for help display
16 | if args.command == "install" or args.command == "pip":
17 | from py_positron import install
18 | install.install(argv)
19 | exit(0)
20 | elif args.command == "start":
21 | from py_positron import startproj as start
22 | start.start(argv)
23 | exit(0)
24 | elif args.command == "update":
25 | from py_positron import updatecmd as update
26 | update.update(argv)
27 | exit(0)
28 | elif args.command == "create":
29 | # Create a parser for create command help
30 | create_parser = argparse.ArgumentParser(prog=f"positron {args.command}")
31 | create_parser.add_argument("--directory", "-d", help="Directory to create the project in (default: current directory)", type=str, default=".")
32 | create_parser.add_argument("--name", "-n", help="Name of the project (default: demo_project)", type=str, default="demo_project")
33 | create_parser.add_argument("--author", "-a", help="Author name (optional)", type=str, default="")
34 | create_parser.add_argument("--description", help="Project description (optional)", type=str, default="")
35 | create_parser.add_argument("--no-venv", help="Do not create a virtual environment", action="store_true")
36 | create_parser.print_help()
37 | exit(0)
38 | elif args.command == "venv":
39 | # Create a parser for venv command help
40 | venv_parser = argparse.ArgumentParser(prog=f"positron {args.command}")
41 | venv_parser.add_argument("--root_path", "-r", help="Root path of the project (default: current directory)", type=str, default=".")
42 | venv_parser.print_help()
43 | exit(0)
44 | elif args.command == "build":
45 | # Create a parser for build command help
46 | build_parser = argparse.ArgumentParser(prog=f"positron {args.command}")
47 | build_parser.add_argument("--root_path", "-r", help="Root path of the project (default: current directory)", type=str, default=".")
48 | build_parser.add_argument("--output", "-o", help="Output filename for the executable", type=str)
49 | build_parser.add_argument("--build-type", help="Type of build to create", type=str, choices=["EXE", "EXE-ONEDIR", "DEB", "DMG", "APK"], default="EXE-ONEDIR")
50 | build_parser.add_argument("--onefile", help="Create a single executable file instead of a directory", action="store_true")
51 | build_parser.add_argument("--windows-console", help="Keep console window on Windows", action="store_true")
52 | build_parser.add_argument("--include-data", help="Include additional data files (format: source=destination)", action="append")
53 | build_parser.add_argument("--python-path", help="Specific Python interpreter to use for building", type=str)
54 | build_parser.print_help()
55 | exit(0)
56 | else:
57 | # For other commands, show basic help
58 | help_parser = argparse.ArgumentParser(prog=f"positron {args.command}")
59 | help_parser.add_argument("--help", "-h", help="Show this help message and exit", action="help")
60 | help_parser.print_help()
61 | exit(0)
62 | if args.version:
63 | import py_positron
64 | print("PyPositron version:", py_positron.__version__)
65 | exit(0)
66 | if args.command=="help":
67 | parser.print_help()
68 | exit(0)
69 | elif args.command=="create":
70 | from py_positron import create
71 | #import create
72 | create.create()
73 | exit(0)
74 | elif args.command=="install" or args.command=="pip":
75 | from py_positron import install
76 | # import install
77 | install.install(argv)
78 | exit(0)
79 | elif args.command=="start":
80 | from py_positron import startproj as start
81 | #import start
82 | start.start(argv)
83 | # elif args.command=="activate":
84 | # #from py_positron import activate
85 | # import activate
86 | # activate.activate()
87 | # exit(0)
88 | elif args.command=="venv":
89 | from py_positron import createvenv
90 | #import createvenv
91 | createvenv.venv()
92 | exit(0)
93 | elif args.command=="update":
94 | from py_positron import updatecmd as update
95 | #import update
96 | update.update(argv)
97 | exit(0)
98 | elif args.command=="build":
99 | from py_positron import buildcmd as build
100 | #import build
101 | build.build(argv)
102 | exit(0)
103 | else:
104 | print("NotImplemented")
105 | exit(0)
106 |
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # PyPositron
6 | ### **PyPositron is currently in alpha stage.** It is not yet ready for production use, but you can try it out and contribute to its development. See more in [Contributing](#contributing).
7 | **PyPositron** is a Python-powered desktop app framework that lets you build cross-platform apps using HTML, CSS, and Python—just like Electron, but with Python as the backend. Write your UI in HTML/CSS, add interactivity with Python, and run your app natively!
8 |
9 |
10 |
11 |
12 | ## Features
13 |
14 | - Build desktop apps using HTML and CSS.
15 | - Use Python for backend and frontend logic. (with support for both Python and JS)
16 | - Use any web framework (like Bootstrap, Tailwind, React, etc.) for your UI.
17 | - Use any HTML builder UI for your app (like Bootstrap Studio, Pinegrow, etc)
18 | - Use JS for compatibility with existing HTML/CSS frameworks.
19 | - Use AI tools for generating your UI without needing proprietary system prompts- simply tell it to generate HTML/CSS/JS UI for your app.
20 | - Virtual environment support.
21 | - Efficient installer creation for easy distribution (that does not exist yet)
22 | (The installer automatically finds an existing browser instead of installing a new one for every app like Electron.JS).
23 |
24 | ## Why PyPositron?
25 | Compared to Electron and other Python frameworks (like PyQt)-
26 | | Feature | PyPositron | Electron | PyQt |
27 | |---------|------------|----------|------|
28 | | Language | Python | **JavaScript, C, C++, etc** | Python |
29 | | UI Frameworks | **Any web technologies** | **Any Web technologies** | Qt Widgets |
30 | | Packaging | **Efficient installer or standalone executable (not yet implemented)** | Electron Builder | PyInstaller etc |
31 | | Performance | **Lightweight** | Heavyweight | **Lightweight** |
32 | | AI Compatibility | **Yes** | **Yes\*** | No\* |
33 | | Compatibility | **All frontend and backend HTML/CSS/JS frameworks and web technologies** | **All frontend and backend HTML/CSS/JS frameworks and web technologies** | Limited to Qt |
34 |
35 | \* maybe
36 |
37 | ## Quick Start
38 |
39 | ### 1. Create a New Project
40 | Install PyPositron if not already installed:
41 | ```bash
42 | pip install py-positron
43 | ```
44 | Them create a new project using the CLI:
45 | ```bash
46 | positron create
47 | # Follow the prompts to set up your project
48 | ```
49 | There should be directories in this structure-
50 |
51 | ```
52 | your_app/
53 | ├── backend
54 | │ └── main.py
55 | ├── frontend/
56 | │ └── index.html
57 | ├── [win/linux]venv/ # If created
58 | │ └──...
59 | ├── LICENSE #MIT by default
60 | ├── config.json
61 | └── ...
62 | ```
63 |
64 | - **backend/main.py**: Entry point for your app.
65 | - **frontend/index.html**: Your app's UI (HTML/CSS/inline Python/JS).
66 | - **winvenv/** or **linuxvenv/**: (Optional) Virtual environment for dependencies.
67 |
68 | ### 2. Run Your App
69 |
70 | ```bash
71 | positron start
72 | ```
73 |
74 | This should open up a window with a checkmark and a button.
75 |
76 | ## CLI Commands
77 |
78 | | Command | Description |
79 | |------------------------------------------|---------------------------------------------------------------|
80 | | `positron create` | Create a new PyPositron project (interactive setup). |
81 | | `positron start [--executable ]` | Run your PyPositron app (optionally specify Python interpreter).|
82 | | `positron install ` | Install a Python package into the project venv. |
83 | | `positron venv` | Create a virtual environment inside your project folder. |
84 |
85 | ## Example Output
86 |
87 |
88 |
89 |
90 |
91 | The default example project created with positron start.
92 |
93 |
94 |
95 |
96 |
97 | A simple example code editor (dark on the right) with the backend code shown (left).
98 |
99 |
100 |
101 |
102 |
103 | ## Documentation & Resources
104 |
105 | - [Official Tutorial & Docs](https://github.com/itzmetanjim/py-positron/wiki)
106 |
107 | ## License and code of conduct.
108 |
109 | This project uses the MIT License. See [LICENSE](LICENSE) for details. If you have any problems with this, feel free to open an issue/dicussion.
110 |
111 | This project follows a [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md).
112 |
113 |
114 | ## Contributing
115 | This project is in alpha stage. Contributions are welcome. See [Contributing](CONTRIBUTING.md) for details.
116 | Things to do-
117 | * [ ] Make documentation and README that is not AI-generated
118 | * [ ] Add more examples and tutorials
119 | * [ ] Make the installer/executable creation system
120 | * [ ] Test on Linux
121 | * [ ] Add support for MacOS
122 | * [ ] Add building and packaging features (like converting to executables/installers that can run on any system without Python installed)
123 | * [ ] Optimize performance.
124 |
125 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[codz]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py.cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | #poetry.toml
110 |
111 | # pdm
112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113 | # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114 | # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115 | #pdm.lock
116 | #pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # pixi
121 | # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122 | #pixi.lock
123 | # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124 | # in the .venv directory. It is recommended not to include this directory in version control.
125 | .pixi
126 |
127 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128 | __pypackages__/
129 |
130 | # Celery stuff
131 | celerybeat-schedule
132 | celerybeat.pid
133 |
134 | # SageMath parsed files
135 | *.sage.py
136 |
137 | # Environments
138 | .env
139 | .envrc
140 | .venv
141 | env/
142 | venv/
143 | ENV/
144 | env.bak/
145 | venv.bak/
146 |
147 | # Spyder project settings
148 | .spyderproject
149 | .spyproject
150 |
151 | # Rope project settings
152 | .ropeproject
153 |
154 | # mkdocs documentation
155 | /site
156 |
157 | # mypy
158 | .mypy_cache/
159 | .dmypy.json
160 | dmypy.json
161 |
162 | # Pyre type checker
163 | .pyre/
164 |
165 | # pytype static type analyzer
166 | .pytype/
167 |
168 | # Cython debug symbols
169 | cython_debug/
170 |
171 | # Covers JetBrains IDEs: IntelliJ, GoLand, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
172 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
173 |
174 | # User-specific stuff
175 | .idea/**/workspace.xml
176 | .idea/**/tasks.xml
177 | .idea/**/usage.statistics.xml
178 | .idea/**/dictionaries
179 | .idea/**/shelf
180 |
181 | # AWS User-specific
182 | .idea/**/aws.xml
183 |
184 | # Generated files
185 | .idea/**/contentModel.xml
186 |
187 | # Sensitive or high-churn files
188 | .idea/**/dataSources/
189 | .idea/**/dataSources.ids
190 | .idea/**/dataSources.local.xml
191 | .idea/**/sqlDataSources.xml
192 | .idea/**/dynamic.xml
193 | .idea/**/uiDesigner.xml
194 | .idea/**/dbnavigator.xml
195 |
196 | # Gradle
197 | .idea/**/gradle.xml
198 | .idea/**/libraries
199 |
200 | # Gradle and Maven with auto-import
201 | # When using Gradle or Maven with auto-import, you should exclude module files,
202 | # since they will be recreated, and may cause churn. Uncomment if using
203 | # auto-import.
204 | # .idea/artifacts
205 | # .idea/compiler.xml
206 | # .idea/jarRepositories.xml
207 | # .idea/modules.xml
208 | # .idea/*.iml
209 | # .idea/modules
210 | # *.iml
211 | # *.ipr
212 |
213 | # CMake
214 | cmake-build-*/
215 |
216 | # Mongo Explorer plugin
217 | .idea/**/mongoSettings.xml
218 |
219 | # File-based project format
220 | *.iws
221 |
222 | # IntelliJ
223 | out/
224 |
225 | # mpeltonen/sbt-idea plugin
226 | .idea_modules/
227 |
228 | # JIRA plugin
229 | atlassian-ide-plugin.xml
230 |
231 | # Cursive Clojure plugin
232 | .idea/replstate.xml
233 |
234 | # SonarLint plugin
235 | .idea/sonarlint/
236 | .idea/sonarlint.xml # see https://community.sonarsource.com/t/is-the-file-idea-idea-idea-sonarlint-xml-intended-to-be-under-source-control/121119
237 |
238 | # Crashlytics plugin (for Android Studio and IntelliJ)
239 | com_crashlytics_export_strings.xml
240 | crashlytics.properties
241 | crashlytics-build.properties
242 | fabric.properties
243 |
244 | # Editor-based HTTP Client
245 | .idea/httpRequests
246 | http-client.private.env.json
247 |
248 | # Android studio 3.1+ serialized cache file
249 | .idea/caches/build_file_checksums.ser
250 |
251 | # Apifox Helper cache
252 | .idea/.cache/.Apifox_Helper
253 | .idea/ApifoxUploaderProjectSetting.xml
254 |
255 | # Abstra
256 | # Abstra is an AI-powered process automation framework.
257 | # Ignore directories containing user credentials, local state, and settings.
258 | # Learn more at https://abstra.io/docs
259 | .abstra/
260 |
261 | # Visual Studio Code
262 | .vscode/*
263 | !.vscode/settings.json
264 | !.vscode/tasks.json
265 | !.vscode/launch.json
266 | !.vscode/extensions.json
267 | !.vscode/*.code-snippets
268 | !*.code-workspace
269 |
270 | # Built Visual Studio Code Extensions
271 | *.vsix
272 |
273 | # Ruff stuff:
274 | .ruff_cache/
275 |
276 | # PyPI configuration file
277 | .pypirc
278 |
279 | # Cursor
280 | # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
281 | # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
282 | # refer to https://docs.cursor.com/context/ignore-files
283 | .cursorignore
284 | .cursorindexingignore
285 |
286 | # Marimo
287 | marimo/_static/
288 | marimo/_lsp/
289 | __marimo__/
290 |
--------------------------------------------------------------------------------
/src/py_positron/startproj.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import json
4 | import platform
5 | import subprocess
6 | from pathlib2 import Path
7 | import argparse
8 | import re
9 | from packaging import version
10 |
11 | def get_current_positron_version():
12 | """Get the version of the currently running PyPositron instance."""
13 | try:
14 | # Import the current PyPositron package to get its version
15 | import py_positron
16 | return py_positron.__version__
17 | except (ImportError, AttributeError):
18 | return None
19 |
20 | def get_venv_positron_version(python_executable):
21 | """Get the PyPositron version installed in the specified Python environment."""
22 | try:
23 | # Run a command to get the version from the target environment
24 | cmd = [python_executable, "-c", "import py_positron; print(py_positron.__version__)"]
25 | result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
26 |
27 | if result.returncode == 0:
28 | return result.stdout.strip()
29 | else:
30 | return None
31 | except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
32 | return None
33 |
34 | def compare_versions(current_version, venv_version):
35 | """Compare two version strings. Returns True if current > venv, False otherwise."""
36 | try:
37 | return version.parse(current_version) > version.parse(venv_version)
38 | except Exception:
39 | return False
40 |
41 | def warn_version_mismatch(current_version, venv_version):
42 | """Display a warning about version mismatch."""
43 | print(f"\x1b[0;93;49m[WARN] Version mismatch detected!\x1b[0m")
44 | print(f" Current PyPositron: v{current_version}")
45 | print(f" Virtual environment PyPositron: v{venv_version}")
46 | print(f" Your virtual environment has an older version of PyPositron.")
47 | print(f" Run 'positron update' to update the virtual environment to the latest version.\x1b[0m")
48 | print()
49 |
50 | def check_and_warn_version_mismatch(python_executable):
51 | """Check for version mismatch and warn the user if necessary."""
52 | current_version = get_current_positron_version()
53 | if not current_version:
54 | return # Can't check if we don't know current version
55 |
56 | venv_version = get_venv_positron_version(python_executable)
57 | if not venv_version:
58 | return # Can't check if we can't get venv version
59 |
60 | if compare_versions(current_version, venv_version):
61 | warn_version_mismatch(current_version, venv_version)
62 |
63 | def check_and_warn_version_mismatch_linux(venv_path):
64 | """Check for version mismatch in Linux venv and warn the user if necessary."""
65 | current_version = get_current_positron_version()
66 | if not current_version:
67 | return # Can't check if we don't know current version
68 |
69 | python_executable = os.path.join(venv_path, "bin", "python3")
70 | if os.path.exists(python_executable):
71 | venv_version = get_venv_positron_version(python_executable)
72 | if venv_version and compare_versions(current_version, venv_version):
73 | warn_version_mismatch(current_version, venv_version)
74 |
75 | def start(argv):
76 | parser = argparse.ArgumentParser()
77 | parser.add_argument("--no-venv", action="store_true", help="Run without venv, even if a compatible venv is present.")
78 | # Default to 'python' on Windows (to use PATH python.exe) and 'python3' elsewhere
79 | default_exe = "python" if platform.system().lower().startswith("win") else "python3"
80 | parser.add_argument(
81 | "--executable",
82 | default=default_exe,
83 | help=f"Python executable to use (default: {default_exe})"
84 | )
85 | args=parser.parse_args(argv)
86 | if not os.path.exists("config.json"):
87 | print("This folder does not contain a PyPositron project. Please navigate to the project root, where config.json is located.")
88 | print("You can create a new project with PyPositron create.")
89 | sys.exit(1)
90 | with open(os.path.abspath("config.json"), "r") as f:
91 | config = json.load(f) # switch CWD to project root so all relative paths (entry_file, venvs) resolve correctly
92 | os.chdir(os.path.dirname(os.path.abspath("config.json")))
93 | if not os.path.exists(config["entry_file"]):
94 | print(f"The entry file {config['entry_file']} does not exist. Please create it or change the entry file path in config.json.")
95 | sys.exit(1)
96 | if not args.no_venv:
97 | if platform.system().lower().strip() == "windows":
98 | ps1_path = Path(__file__).parent / "python_executor.ps1"
99 | absolute = ps1_path.resolve()
100 | if config["has_venv"]:
101 | if os.path.exists(config.get("winvenv_executable","")) and config.get("winvenv_executable","") != "":
102 | # Check for version mismatch before running
103 | check_and_warn_version_mismatch(config["winvenv_executable"])
104 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}' '{config['entry_file']}' '{os.getcwd()}' '{config['winvenv_executable']}'"]
105 | result = subprocess.run(cmd,check=True)
106 | if result.returncode != 0:
107 | print("Error:", result.stderr, file=sys.stderr)
108 | sys.exit(result.returncode)
109 | sys.exit(0)
110 | else:
111 | print("\x1b[0;93;49m[WARN]Running without venv, as this project does not contain a windows venv, but has a linux venv.\x1b[0m")
112 | # Use the executable from command line argument instead of config
113 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}' '{config['entry_file']}' '{os.getcwd()}' '{args.executable}'"]
114 | result = subprocess.run(cmd,check=True)
115 | if result.returncode != 0:
116 | print("Error:", result.stderr, file=sys.stderr)
117 | sys.exit(result.returncode)
118 | sys.exit(0)
119 | else:
120 | if config["has_venv"]:
121 | if os.path.exists(config.get("linuxvenv","")) and config.get("linuxvenv","") != "":
122 | # Check for version mismatch before running
123 | check_and_warn_version_mismatch_linux(config["linuxvenv"])
124 | os.system("bash -c \'source \""+config["linuxvenv"]+"/bin/activate\" && "+args.executable+" \""+os.path.abspath(config["entry_file"])+"\"\'")
125 | exit(0)
126 | else:
127 | print("\x1b[0;93;49m[WARN]Running without venv, as this project does not contain a linux venv, but has a Windows venv.\x1b[0m")
128 | else:
129 | os.system(args.executable+" \""+os.path.abspath(config["entry_file"])+"\"")
130 | exit(0)
131 | else:
132 | if platform.system().lower().strip() == "windows":
133 | ps1_path = Path(__file__).parent / "python_executor.ps1"
134 | absolute = ps1_path.resolve()
135 | # Use the executable from command line argument instead of config
136 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}' '{config['entry_file']}' '{os.getcwd()}' '{args.executable}'"]
137 | result = subprocess.run(cmd,check=True)
138 | if result.returncode != 0:
139 | print("Error:", result.stderr, file=sys.stderr)
140 | sys.exit(result.returncode)
141 | else:
142 | os.system(args.executable+" \""+os.path.abspath(config["entry_file"])+"\"")
143 | exit(0)
144 |
--------------------------------------------------------------------------------
/src/py_positron/create.py:
--------------------------------------------------------------------------------
1 | #!usr/bin/env python3
2 | import os
3 | import sys
4 | import json
5 | import platform
6 | import datetime
7 | import subprocess
8 | from pathlib2 import Path
9 | import argparse
10 | import py_positron
11 | def create():
12 | license_template="""
13 | Copyright {text}
14 |
15 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
16 |
17 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 | """
21 | checkmarkpng_path = os.path.join(os.path.dirname(__file__), "example/checkmark.png")
22 | mainpy=""
23 | indexhtml=""
24 | with open(os.path.join(os.path.dirname(__file__), "example/main_app.py"), "r") as f:
25 | mainpy=f.read()
26 | with open(os.path.join(os.path.dirname(__file__), "example/index.html"), "r") as f:
27 | indexhtml=f.read()
28 | print("Welcome to the PyPositron project creation wizard!\nPress ^C to exit.")
29 | try:
30 | directory = input("\033[1;92;49mProject directory (leave empty to use current directory):\033[0m ")
31 | if directory =="":
32 | print("Using current directory.")
33 | directory = os.getcwd()
34 | try:
35 | os.makedirs(directory, exist_ok=True)
36 | except OSError as e:
37 | print(f"\033[1;91;49m[ERROR] Invalid directory syntax: {e}\033[0m")
38 | exit(1)
39 | print("You can change any of the following options later in \033[4;39;49mconfig.json\033[0m.")
40 | projname=input("\033[1;92;49mProject name (default: demo_project):\033[0m ")
41 | if projname == "":
42 | projname = "demo_project"
43 | authorname=input("\033[1;92;49mAuthor name (optional):\033[0m ")
44 | description=input("\033[1;92;49mProject description (optional):\033[0m ")
45 | project_version=input("\033[1;92;49mProject version (default: 0.1.0):\033[0m ")
46 | if project_version == "":
47 | project_version = "0.1.0"
48 | print("A virtual environment (venv) can be used to isolate the project enviroment from the main python installation.")
49 | print("This can be useful to avoid conflicts between different projects and packages.")
50 | print("The venv will be automatically activated and deactivated by PyPositron.")
51 | print("It is recommended to use a venv for most projects, but you will need to install all your packages seperately on the venv.")
52 | print("If you are using Mac, you have to create a venv manually https://github.com/itzmetanjim/py-PyPositron/wiki/Manually-creating-a-venv")
53 | print("You can create a venv in your project later by using \033[4;39;49mpositron venv\033[0m.")
54 | venv_choice=input("\033[1;92;49mCreate a venv inside the project directory? y/N: \033[0m ")
55 | while venv_choice.lower() not in ["y", "n", "yes", "no",""]:
56 | print("\033[1;91;49m[ERROR] Invalid input. Please enter y or n.\033[0m")
57 | venv_choice=input("\033[1;92;49mCreate a venv inside the project directory? y/N: \033[0m ")
58 | venv=False
59 | if venv_choice.lower() in ["y", "yes"]:
60 | venv = True
61 | elif venv_choice.lower() in ["n", "no", ""]:
62 | venv = False
63 | if venv:
64 | print("You can install libraries in a PyPositron project venv by using positron pip install .")
65 | print("The PyPositron package will be automatically installed in the venv when the project is created.")
66 | print("If it fails, create a venv using positron venv.")
67 | print("\033[1;92;49mSelected options (can be changed):\033[0m")
68 | print("\033[1;92;49mDirectory:\033[0m ", directory)
69 | print("\033[1;92;49mProject name:\033[0m ", projname)
70 | print("\033[1;92;49mAuthor name:\033[0m ", authorname)
71 | print("\033[1;92;49mDescription:\033[0m ", description)
72 | print("\033[1;92;49mA venv will be created.\033[0m " if venv else "\033[1;92;49mNo venv will be created, but you can create one later.\033[0m ")
73 | print("You can change these options and more through \033[4;39;49mconfig.json\033[0m.")
74 | input("\033[1;92;49mPress Enter to continue or ^C to exit and change the options\033[0m")
75 | print("Creating project...")
76 | config={
77 | "name": projname,
78 | "author": authorname,
79 | "description": description,
80 | "has_venv": venv,
81 | "requirements":"requirements.txt",
82 | "entry_file":"backend/main.py",
83 | "positron_version":py_positron.__version__, #Placeholder, replace with positron.__version__
84 | "python_version":f"{sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]}",
85 | "project_version": project_version,
86 | "winvenv_executable": "winvenv/Scripts/python.exe" if venv and platform.system().lower().strip()=="windows" else "",
87 | "linuxvenv":"",
88 | "license_type": "MIT",
89 | "license_file": "LICENSE",
90 | }
91 | except KeyboardInterrupt:
92 | exit(0)
93 | except EOFError:
94 | exit(0)
95 | if platform.system().lower().strip() == "linux":
96 | config.update({"linuxvenv": "linuxvenv" if venv else ""})
97 | print("Creating config.json...")
98 | with open(os.path.join(os.path.abspath(directory), "config.json"), "w") as f:
99 | json.dump(config, f, indent=4)
100 | print("Creating license file...")
101 | with open(os.path.join(os.path.abspath(directory), "LICENSE"), "w") as f:
102 | f.write(license_template.format(text=str(datetime.datetime.now().year)+" "+authorname))
103 | print("Creating directories and example files...")
104 | print("Creating backend...")
105 | try:
106 | os.mkdir(os.path.join(os.path.abspath(directory), "backend"))
107 | except FileExistsError:
108 | pass
109 | print("Creating frontend...")
110 | try:
111 | os.mkdir(os.path.join(os.path.abspath(directory), "frontend"))
112 | except FileExistsError:
113 | pass
114 | print("Creating example files...")
115 | with open(os.path.join(os.path.abspath(directory), "backend/main.py"), "w") as f:
116 | f.write(mainpy)
117 | with open(os.path.join(os.path.abspath(directory), "frontend/index.html"), "w") as f:
118 | f.write(indexhtml)
119 | with open(os.path.join(os.path.abspath(directory), "frontend/checkmark.png"), "wb") as f:
120 | with open(checkmarkpng_path, "rb") as f2:
121 | f.write(f2.read())
122 | print("Changing to the project directory...")
123 | os.chdir(os.path.abspath(directory))
124 | if venv:
125 | if platform.system().lower().strip() == "windows":
126 | print("Creating venv and installing PyPositron (this may take a while)...")
127 | ps1_path = Path(__file__).parent / "winvenv_setup.ps1"
128 | absolute = ps1_path.resolve()
129 | cmd = ["powershell","-NoProfile","-ExecutionPolicy", "Bypass","-Command", f"& '{absolute}'"]
130 | result = subprocess.run(cmd, capture_output=True, text=True)
131 | if result.returncode != 0:
132 | print("Error:", result.stderr, file=sys.stderr)
133 | print("Failed to create venv. Please create a venv manually and install PyPositron in it.")
134 | sys.exit(result.returncode)
135 | print("PyPositron successfully installed in venv.")
136 |
137 | elif platform.system().lower().strip() == "linux":
138 | print("Creating venv (this may take a while)...")
139 | os.system("python3 -m venv ./linuxvenv/")
140 | print("Activating venv...")
141 | os.system("bash -c \"source linuxvenv/bin/activate\"")
142 | print("Installing PyPositron in venv- if this fails, use pip to install it...")
143 | os.system('bash -c "source linuxvenv/bin/activate && pip3 install -U pip"')
144 | os.system('bash -c "source linuxvenv/bin/activate && pip3 install -U py_positron"')
145 | print("PyPositron installed in venv.")
146 | elif platform.system().lower().strip() == "darwin":
147 | print("Venv creation is not supported on MacOS through the project creation wizard.\nSee https://github.com/itzmetanjim/py-positron/wiki/Manually-creating-a-venv.")
148 | else:
149 | print("""Could not create venv- Unsupported OS. Please create a venv manually and install PyPositron in it. See https://github.com/itzmetanjim/py-positron/wiki/Manually-creating-a-venv.""")
150 | print("\033[1;92;49mProject created successfully!\033[0m")
151 | print("\033[1;92;49mYou can now run your project with:\033[0m")
152 | print("\033[4;39;49mpositron start\033[0m")
153 |
--------------------------------------------------------------------------------
/src/py_positron/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.0.2.3"
2 | import webview
3 | import os
4 | import threading
5 | import re
6 | import importlib
7 | import json
8 | import traceback
9 | import sys
10 | import html
11 | from typing import Callable, Optional
12 | class PositronWindowWrapper:
13 | """Wrapper for a PyPositron window with event loop thread and context."""
14 | def __init__(self, window, context, main_thread):
15 | self.window: webview.Window = window
16 | self.context: PositronContext = context
17 | self.document = Document(window)
18 | self.exposed = ExposedFunctions(context.exposed_functions)
19 | self.event_thread: threading.Thread = main_thread
20 | self.htmlwindow = HTMLWindow(window)
21 |
22 | def escape_js_string(string: str) -> str:
23 | """Escape string for JavaScript"""
24 | return string.replace("\\", "\\\\").replace("\"", "\\\"").replace("\'", "\\'").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace("\f", "\\f").replace("\v", "\\v")
25 |
26 | class PositronContext:
27 | def __init__(self, window):
28 | self.window = window
29 | self.globals = {}
30 | self.locals = {}
31 | self.exposed_functions = {}
32 | self.event_handlers = {}
33 |
34 | def __setattr__(self, name, value):
35 | """Allow dynamic attribute assignment"""
36 | object.__setattr__(self, name, value)
37 | def execute(self, code):
38 | try:
39 | self.locals.update({
40 | "window": self.window,
41 | "document":Document(self.window),
42 | "exposed": ExposedFunctions(self.exposed_functions),
43 | "import_module": importlib.import_module,
44 | })
45 | exec(code, self.globals, self.locals)
46 | self.globals.update(self.locals) #...
47 | return True, None
48 | except Exception as e:
49 | error_info = traceback.format_exc()
50 | return False, error_info
51 | def register_event_handler(self, element_id, event_type, callback):
52 |
53 | key = f"{element_id}_{event_type}"
54 | callback_globals = dict(self.globals)
55 | callback_locals = dict(self.locals)
56 | callback_func = callback
57 | exposed_functions = self.exposed_functions
58 |
59 |
60 | def wrapped_callback():
61 | try:
62 | exec_globals = dict(callback_globals)
63 | exec_locals = dict(callback_locals)
64 | important_stuff={
65 | "window": self.window,
66 | "document": Document(self.window),
67 | "exposed": ExposedFunctions(exposed_functions),
68 | "callback_func": callback_func
69 | }
70 | exec_globals.update(important_stuff)
71 | exec_locals.update(important_stuff)
72 | exec_code = "def _executor():\n return callback_func()"
73 | exec(exec_code, exec_globals, exec_locals)
74 | result = exec_locals["_executor"]()
75 | return result
76 | except Exception as e:
77 | print(f"Error in event handler: {e}")
78 | traceback.print_exc()
79 | return str(e)
80 | self.event_handlers[key] = wrapped_callback
81 | return key
82 | class Element:
83 | def __init__(self, window, js_path):
84 | self.window = window
85 | self.js_path = js_path
86 |
87 | # Properties
88 | @property
89 | def accessKey(self) -> str:
90 | """Sets or returns the accesskey attribute of an element"""
91 | return self.window.evaluate_js(f'{self.js_path}.accessKey')
92 |
93 | @accessKey.setter
94 | def accessKey(self, value: str):
95 | """Sets the accesskey attribute of an element"""
96 | self.window.evaluate_js(f'{self.js_path}.accessKey = {json.dumps(value)}')
97 |
98 | @property
99 | def attributes(self):
100 | """Returns a NamedNodeMap of an element's attributes"""
101 | return self.window.evaluate_js(f'{self.js_path}.attributes')
102 |
103 | @property
104 | def childElementCount(self) -> int:
105 | """Returns an element's number of child elements"""
106 | return self.window.evaluate_js(f'{self.js_path}.childElementCount') or 0
107 |
108 | @property
109 | def childNodes(self):
110 | """Returns a NodeList of an element's child nodes"""
111 | return ElementList(self.window, f'{self.js_path}.childNodes')
112 |
113 | @property
114 | def children(self):
115 | """Returns an HTMLCollection of an element's child elements"""
116 | return ElementList(self.window, f'{self.js_path}.children')
117 |
118 | @property
119 | def classList(self):
120 | """Returns the class name(s) of an element"""
121 | return self.window.evaluate_js(f'{self.js_path}.classList')
122 |
123 | @property
124 | def className(self) -> str:
125 | """Sets or returns the value of the class attribute of an element"""
126 | return self.window.evaluate_js(f'{self.js_path}.className')
127 |
128 | @className.setter
129 | def className(self, value: str):
130 | """Sets the value of the class attribute of an element"""
131 | self.window.evaluate_js(f'{self.js_path}.className = {json.dumps(value)}')
132 |
133 | @property
134 | def clientHeight(self) -> int:
135 | """Returns the height of an element, including padding"""
136 | return self.window.evaluate_js(f'{self.js_path}.clientHeight') or 0
137 |
138 | @property
139 | def clientLeft(self) -> int:
140 | """Returns the width of the left border of an element"""
141 | return self.window.evaluate_js(f'{self.js_path}.clientLeft') or 0
142 |
143 | @property
144 | def clientTop(self) -> int:
145 | """Returns the width of the top border of an element"""
146 | return self.window.evaluate_js(f'{self.js_path}.clientTop') or 0
147 |
148 | @property
149 | def clientWidth(self) -> int:
150 | """Returns the width of an element, including padding"""
151 | return self.window.evaluate_js(f'{self.js_path}.clientWidth') or 0
152 |
153 | @property
154 | def contentEditable(self) -> str:
155 | """Sets or returns whether the content of an element is editable or not"""
156 | return self.window.evaluate_js(f'{self.js_path}.contentEditable')
157 |
158 | @contentEditable.setter
159 | def contentEditable(self, value: str):
160 | """Sets whether the content of an element is editable or not"""
161 | self.window.evaluate_js(f'{self.js_path}.contentEditable = {json.dumps(value)}')
162 |
163 | @property
164 | def dir(self) -> str:
165 | """Sets or returns the value of the dir attribute of an element"""
166 | return self.window.evaluate_js(f'{self.js_path}.dir')
167 |
168 | @dir.setter
169 | def dir(self, value: str):
170 | """Sets the value of the dir attribute of an element"""
171 | self.window.evaluate_js(f'{self.js_path}.dir = {json.dumps(value)}')
172 |
173 | @property
174 | def firstChild(self):
175 | """Returns the first child node of an element"""
176 | return Element(self.window, f'{self.js_path}.firstChild')
177 |
178 | @property
179 | def firstElementChild(self):
180 | """Returns the first child element of an element"""
181 | return Element(self.window, f'{self.js_path}.firstElementChild')
182 |
183 | @property
184 | def id(self) -> str:
185 | """Sets or returns the value of the id attribute of an element"""
186 | return self.window.evaluate_js(f'{self.js_path}.id')
187 |
188 | @id.setter
189 | def id(self, value: str):
190 | """Sets the value of the id attribute of an element"""
191 | self.window.evaluate_js(f'{self.js_path}.id = {json.dumps(value)}')
192 |
193 | @property
194 | def isContentEditable(self) -> bool:
195 | """Returns true if an element's content is editable"""
196 | return self.window.evaluate_js(f'{self.js_path}.isContentEditable')
197 |
198 | @property
199 | def lang(self) -> str:
200 | """Sets or returns the value of the lang attribute of an element"""
201 | return self.window.evaluate_js(f'{self.js_path}.lang')
202 |
203 | @lang.setter
204 | def lang(self, value: str):
205 | """Sets the value of the lang attribute of an element"""
206 | self.window.evaluate_js(f'{self.js_path}.lang = {json.dumps(value)}')
207 |
208 | @property
209 | def lastChild(self):
210 | """Returns the last child node of an element"""
211 | return Element(self.window, f'{self.js_path}.lastChild')
212 |
213 | @property
214 | def lastElementChild(self):
215 | """Returns the last child element of an element"""
216 | return Element(self.window, f'{self.js_path}.lastElementChild')
217 |
218 | @property
219 | def namespaceURI(self) -> str:
220 | """Returns the namespace URI of an element"""
221 | return self.window.evaluate_js(f'{self.js_path}.namespaceURI')
222 |
223 | @property
224 | def nextSibling(self):
225 | """Returns the next node at the same node tree level"""
226 | return Element(self.window, f'{self.js_path}.nextSibling')
227 |
228 | @property
229 | def nextElementSibling(self):
230 | """Returns the next element at the same node tree level"""
231 | return Element(self.window, f'{self.js_path}.nextElementSibling')
232 |
233 | @property
234 | def nodeName(self) -> str:
235 | """Returns the name of a node"""
236 | return self.window.evaluate_js(f'{self.js_path}.nodeName')
237 |
238 | @property
239 | def nodeType(self) -> int:
240 | """Returns the node type of a node"""
241 | return self.window.evaluate_js(f'{self.js_path}.nodeType') or 0
242 |
243 | @property
244 | def nodeValue(self) -> str:
245 | """Sets or returns the value of a node"""
246 | return self.window.evaluate_js(f'{self.js_path}.nodeValue')
247 |
248 | @nodeValue.setter
249 | def nodeValue(self, value: str):
250 | """Sets the value of a node"""
251 | self.window.evaluate_js(f'{self.js_path}.nodeValue = {json.dumps(value)}')
252 |
253 | @property
254 | def offsetHeight(self) -> int:
255 | """Returns the height of an element, including padding, border and scrollbar"""
256 | return self.window.evaluate_js(f'{self.js_path}.offsetHeight') or 0
257 |
258 | @property
259 | def offsetWidth(self) -> int:
260 | """Returns the width of an element, including padding, border and scrollbar"""
261 | return self.window.evaluate_js(f'{self.js_path}.offsetWidth') or 0
262 |
263 | @property
264 | def offsetLeft(self) -> int:
265 | """Returns the horizontal offset position of an element"""
266 | return self.window.evaluate_js(f'{self.js_path}.offsetLeft') or 0
267 |
268 | @property
269 | def offsetParent(self):
270 | """Returns the offset container of an element"""
271 | return Element(self.window, f'{self.js_path}.offsetParent')
272 |
273 | @property
274 | def offsetTop(self) -> int:
275 | """Returns the vertical offset position of an element"""
276 | return self.window.evaluate_js(f'{self.js_path}.offsetTop') or 0
277 |
278 | @property
279 | def outerHTML(self) -> str:
280 | """Sets or returns the content of an element (including the start tag and the end tag)"""
281 | return self.window.evaluate_js(f'{self.js_path}.outerHTML')
282 |
283 | @outerHTML.setter
284 | def outerHTML(self, value: str):
285 | """Sets the content of an element (including the start tag and the end tag)"""
286 | self.window.evaluate_js(f'{self.js_path}.outerHTML = {json.dumps(value)}')
287 |
288 | @property
289 | def outerText(self) -> str:
290 | """Sets or returns the outer text content of a node and its descendants"""
291 | return self.window.evaluate_js(f'{self.js_path}.outerText')
292 |
293 | @outerText.setter
294 | def outerText(self, value: str):
295 | """Sets the outer text content of a node and its descendants"""
296 | self.window.evaluate_js(f'{self.js_path}.outerText = {json.dumps(value)}')
297 |
298 | @property
299 | def ownerDocument(self):
300 | """Returns the root element (document object) for an element"""
301 | return self.window.evaluate_js(f'{self.js_path}.ownerDocument')
302 |
303 | @property
304 | def parentNode(self):
305 | """Returns the parent node of an element"""
306 | return Element(self.window, f'{self.js_path}.parentNode')
307 |
308 | @property
309 | def parentElement(self):
310 | """Returns the parent element node of an element"""
311 | return Element(self.window, f'{self.js_path}.parentElement')
312 |
313 | @property
314 | def previousSibling(self):
315 | """Returns the previous node at the same node tree level"""
316 | return Element(self.window, f'{self.js_path}.previousSibling')
317 |
318 | @property
319 | def previousElementSibling(self):
320 | """Returns the previous element at the same node tree level"""
321 | return Element(self.window, f'{self.js_path}.previousElementSibling')
322 |
323 | @property
324 | def scrollHeight(self) -> int:
325 | """Returns the entire height of an element, including padding"""
326 | return self.window.evaluate_js(f'{self.js_path}.scrollHeight') or 0
327 |
328 | @property
329 | def scrollLeft(self) -> int:
330 | """Sets or returns the number of pixels an element's content is scrolled horizontally"""
331 | return self.window.evaluate_js(f'{self.js_path}.scrollLeft') or 0
332 |
333 | @scrollLeft.setter
334 | def scrollLeft(self, value: int):
335 | """Sets the number of pixels an element's content is scrolled horizontally"""
336 | self.window.evaluate_js(f'{self.js_path}.scrollLeft = {value}')
337 |
338 | @property
339 | def scrollTop(self) -> int:
340 | """Sets or returns the number of pixels an element's content is scrolled vertically"""
341 | return self.window.evaluate_js(f'{self.js_path}.scrollTop') or 0
342 |
343 | @scrollTop.setter
344 | def scrollTop(self, value: int):
345 | """Sets the number of pixels an element's content is scrolled vertically"""
346 | self.window.evaluate_js(f'{self.js_path}.scrollTop = {value}')
347 |
348 | @property
349 | def scrollWidth(self) -> int:
350 | """Returns the entire width of an element, including padding"""
351 | return self.window.evaluate_js(f'{self.js_path}.scrollWidth') or 0
352 |
353 | @property
354 | def tabIndex(self) -> int:
355 | """Sets or returns the value of the tabindex attribute of an element"""
356 | return self.window.evaluate_js(f'{self.js_path}.tabIndex') or 0
357 |
358 | @tabIndex.setter
359 | def tabIndex(self, value: int):
360 | """Sets the value of the tabindex attribute of an element"""
361 | self.window.evaluate_js(f'{self.js_path}.tabIndex = {value}')
362 |
363 | @property
364 | def tagName(self) -> str:
365 | """Returns the tag name of an element"""
366 | return self.window.evaluate_js(f'{self.js_path}.tagName')
367 |
368 | @property
369 | def textContent(self) -> str:
370 | """Sets or returns the textual content of a node and its descendants"""
371 | return self.window.evaluate_js(f'{self.js_path}.textContent')
372 |
373 | @textContent.setter
374 | def textContent(self, value: str):
375 | """Sets the textual content of a node and its descendants"""
376 | self.window.evaluate_js(f'{self.js_path}.textContent = {json.dumps(value)}')
377 |
378 | @property
379 | def title(self) -> str:
380 | """Sets or returns the value of the title attribute of an element"""
381 | return self.window.evaluate_js(f'{self.js_path}.title')
382 |
383 | @title.setter
384 | def title(self, value: str):
385 | """Sets the value of the title attribute of an element"""
386 | self.window.evaluate_js(f'{self.js_path}.title = {json.dumps(value)}')
387 |
388 | # Existing properties
389 | @property
390 | def innerText(self) -> str:
391 | """Get inner text"""
392 | return self.window.evaluate_js(f'{self.js_path}.innerText')
393 | @innerText.setter
394 | def innerText(self, value):
395 | """Set inner text"""
396 | self.window.evaluate_js(f'{self.js_path}.innerText = {json.dumps(value)}')
397 |
398 | @property
399 | def innerHTML(self) -> str:
400 | """Get inner HTML"""
401 | return self.window.evaluate_js(f'{self.js_path}.innerHTML')
402 | @innerHTML.setter
403 | def innerHTML(self, value):
404 | """Set inner HTML - Warning: this can lead to XSS vulnerabilities if not sanitized properly. Use with caution."""
405 | self.window.evaluate_js(f'{self.js_path}.innerHTML = {json.dumps(value)}')
406 |
407 | @property
408 | def value(self):
409 | """Get value of form element."""
410 | return self.window.evaluate_js(f'{self.js_path}.value')
411 | @value.setter
412 | def value(self, value):
413 | """Set value of form element."""
414 | self.window.evaluate_js(f'{self.js_path}.value = {json.dumps(value)}')
415 |
416 | @property
417 | def style(self):
418 | """Get style object"""
419 | return Style(self.window, f'{self.js_path}.style')
420 |
421 | # Methods
422 | def after(self, *nodes):
423 | """Inserts one or more nodes (elements) or strings after an element"""
424 | for node in nodes:
425 | if isinstance(node, Element):
426 | self.window.evaluate_js(f'{self.js_path}.after({node.js_path})')
427 | else:
428 | self.window.evaluate_js(f'{self.js_path}.after({json.dumps(str(node))})')
429 |
430 | def append(self, *nodes):
431 | """Adds (appends) one or several nodes (element) or strings after the last child of an element"""
432 | for node in nodes:
433 | if isinstance(node, Element):
434 | self.window.evaluate_js(f'{self.js_path}.append({node.js_path})')
435 | else:
436 | self.window.evaluate_js(f'{self.js_path}.append({json.dumps(str(node))})')
437 |
438 | def before(self, *nodes):
439 | """Inserts one or more nodes (elements) or strings before an element"""
440 | for node in nodes:
441 | if isinstance(node, Element):
442 | self.window.evaluate_js(f'{self.js_path}.before({node.js_path})')
443 | else:
444 | self.window.evaluate_js(f'{self.js_path}.before({json.dumps(str(node))})')
445 |
446 | def blur(self):
447 | """Removes focus from an element"""
448 | self.window.evaluate_js(f'{self.js_path}.blur()')
449 |
450 | def click(self):
451 | """Simulates a mouse-click on an element"""
452 | self.window.evaluate_js(f'{self.js_path}.click()')
453 |
454 | def cloneNode(self, deep: bool = False):
455 | """Clones an element"""
456 | return Element(self.window, f'{self.js_path}.cloneNode({json.dumps(deep)})')
457 |
458 | def closest(self, selector: str):
459 | """Searches the DOM tree for the closest element that matches a CSS selector"""
460 | return Element(self.window, f'{self.js_path}.closest({json.dumps(selector)})')
461 |
462 | def compareDocumentPosition(self, other):
463 | """Compares the document position of two elements"""
464 | if isinstance(other, Element):
465 | return self.window.evaluate_js(f'{self.js_path}.compareDocumentPosition({other.js_path})')
466 | else:
467 | raise TypeError("compareDocumentPosition expects an Element")
468 |
469 | def contains(self, other):
470 | """Returns true if a node is a descendant of a node"""
471 | if isinstance(other, Element):
472 | return self.window.evaluate_js(f'{self.js_path}.contains({other.js_path})')
473 | else:
474 | raise TypeError("contains expects an Element")
475 |
476 | def focus(self):
477 | """Gives focus to an element"""
478 | self.window.evaluate_js(f'{self.js_path}.focus()')
479 |
480 | def getAttribute(self, name: str) -> str:
481 | """Returns the value of an element's attribute"""
482 | return self.window.evaluate_js(f'{self.js_path}.getAttribute({json.dumps(name)})')
483 |
484 | def getAttributeNode(self, name: str):
485 | """Returns an attribute node"""
486 | return self.window.evaluate_js(f'{self.js_path}.getAttributeNode({json.dumps(name)})')
487 |
488 | def getBoundingClientRect(self):
489 | """Returns the size of an element and its position relative to the viewport"""
490 | return self.window.evaluate_js(f'{self.js_path}.getBoundingClientRect()')
491 |
492 | def getElementsByClassName(self, class_name: str):
493 | """Returns a collection of child elements with a given class name"""
494 | return ElementList(self.window, f'{self.js_path}.getElementsByClassName({json.dumps(class_name)})')
495 |
496 | def getElementsByTagName(self, tag_name: str):
497 | """Returns a collection of child elements with a given tag name"""
498 | return ElementList(self.window, f'{self.js_path}.getElementsByTagName({json.dumps(tag_name)})')
499 |
500 | def hasAttribute(self, name: str) -> bool:
501 | """Returns true if an element has a given attribute"""
502 | return self.window.evaluate_js(f'{self.js_path}.hasAttribute({json.dumps(name)})')
503 |
504 | def hasAttributes(self) -> bool:
505 | """Returns true if an element has any attributes"""
506 | return self.window.evaluate_js(f'{self.js_path}.hasAttributes()')
507 |
508 | def hasChildNodes(self) -> bool:
509 | """Returns true if an element has any child nodes"""
510 | return self.window.evaluate_js(f'{self.js_path}.hasChildNodes()')
511 |
512 | def insertAdjacentElement(self, position: str, element):
513 | """Inserts a new HTML element at a position relative to an element"""
514 | if isinstance(element, Element):
515 | return Element(self.window, f'{self.js_path}.insertAdjacentElement({json.dumps(position)}, {element.js_path})')
516 | else:
517 | raise TypeError("insertAdjacentElement expects an Element")
518 |
519 | def insertAdjacentHTML(self, position: str, html: str):
520 | """Inserts an HTML formatted text at a position relative to an element"""
521 | self.window.evaluate_js(f'{self.js_path}.insertAdjacentHTML({json.dumps(position)}, {json.dumps(html)})')
522 |
523 | def insertAdjacentText(self, position: str, text: str):
524 | """Inserts text into a position relative to an element"""
525 | self.window.evaluate_js(f'{self.js_path}.insertAdjacentText({json.dumps(position)}, {json.dumps(text)})')
526 |
527 | def insertBefore(self, new_node, reference_node):
528 | """Inserts a new child node before an existing child node"""
529 | if isinstance(new_node, Element) and isinstance(reference_node, Element):
530 | return Element(self.window, f'{self.js_path}.insertBefore({new_node.js_path}, {reference_node.js_path})')
531 | else:
532 | raise TypeError("insertBefore expects Element objects")
533 |
534 | def isDefaultNamespace(self, namespace_uri: str) -> bool:
535 | """Returns true if a given namespaceURI is the default"""
536 | return self.window.evaluate_js(f'{self.js_path}.isDefaultNamespace({json.dumps(namespace_uri)})')
537 |
538 | def isEqualNode(self, other):
539 | """Checks if two elements are equal"""
540 | if isinstance(other, Element):
541 | return self.window.evaluate_js(f'{self.js_path}.isEqualNode({other.js_path})')
542 | else:
543 | raise TypeError("isEqualNode expects an Element")
544 |
545 | def isSameNode(self, other):
546 | """Checks if two elements are the same node"""
547 | if isinstance(other, Element):
548 | return self.window.evaluate_js(f'{self.js_path}.isSameNode({other.js_path})')
549 | else:
550 | raise TypeError("isSameNode expects an Element")
551 |
552 | def matches(self, selector: str) -> bool:
553 | """Returns true if an element is matched by a given CSS selector"""
554 | return self.window.evaluate_js(f'{self.js_path}.matches({json.dumps(selector)})')
555 |
556 | def normalize(self):
557 | """Joins adjacent text nodes and removes empty text nodes in an element"""
558 | self.window.evaluate_js(f'{self.js_path}.normalize()')
559 |
560 | def querySelector(self, selector: str):
561 | """Returns the first child element that matches a CSS selector(s)"""
562 | return Element(self.window, f'{self.js_path}.querySelector({json.dumps(selector)})')
563 |
564 | def querySelectorAll(self, selector: str):
565 | """Returns all child elements that matches a CSS selector(s)"""
566 | return ElementList(self.window, f'{self.js_path}.querySelectorAll({json.dumps(selector)})')
567 |
568 | def remove(self):
569 | """Removes an element from the DOM"""
570 | self.window.evaluate_js(f'{self.js_path}.remove()')
571 |
572 | def removeAttribute(self, name: str):
573 | """Removes an attribute from an element"""
574 | self.window.evaluate_js(f'{self.js_path}.removeAttribute({json.dumps(name)})')
575 |
576 | def removeAttributeNode(self, attr_node):
577 | """Removes an attribute node, and returns the removed node"""
578 | return self.window.evaluate_js(f'{self.js_path}.removeAttributeNode({attr_node})')
579 |
580 | def removeEventListener(self, event_type: str, callback=None):
581 | """Removes an event handler that has been attached with the addEventListener() method"""
582 | # Get element ID for handler removal
583 | element_id = self.window.evaluate_js(f"""
584 | (function() {{
585 | var el = {self.js_path};
586 | return el ? (el.id || 'anonymous_' + Math.random().toString(36).substr(2, 9)) : null;
587 | }})()
588 | """)
589 |
590 | if element_id and hasattr(self.window, '_py_context'):
591 | key = f"{element_id}_{event_type}"
592 | if key in self.window._py_context.event_handlers:
593 | del self.window._py_context.event_handlers[key]
594 |
595 | # Note: JavaScript removeEventListener requires the exact same function reference
596 | # This is a simplified implementation
597 | js_code = f"""
598 | console.log("removeEventListener called for {event_type} on element");
599 | """
600 | self.window.evaluate_js(js_code)
601 |
602 | def scrollIntoView(self, align_to_top: bool = True):
603 | """Scrolls the element into the visible area of the browser window"""
604 | self.window.evaluate_js(f'{self.js_path}.scrollIntoView({json.dumps(align_to_top)})')
605 |
606 | def setAttributeNode(self, attr_node):
607 | """Sets or changes an attribute node"""
608 | return self.window.evaluate_js(f'{self.js_path}.setAttributeNode({attr_node})')
609 |
610 | def toString(self) -> str:
611 | """Converts an element to a string"""
612 | return self.window.evaluate_js(f'{self.js_path}.toString()')
613 |
614 | # Existing methods
615 | def setAttribute(self, attr_name, value):
616 | """Set attribute"""
617 | self.window.evaluate_js(f'{self.js_path}.setAttribute("{attr_name}", {json.dumps(value)})')
618 |
619 | def appendChild(self, child):
620 | """Append child"""
621 | if isinstance(child, Element):
622 | self.window.evaluate_js(f'{self.js_path}.appendChild({child.js_path})')
623 | else:
624 | raise TypeError("appendChild expects an Element")
625 | def removeChild(self, child):
626 | """Remove child"""
627 | if isinstance(child, Element):
628 | self.window.evaluate_js(f'{self.js_path}.removeChild({child.js_path})')
629 | else:
630 | raise TypeError("removeChild expects an Element")
631 |
632 | def replaceChild(self, new_child, old_child):
633 | """Replace child"""
634 | if isinstance(new_child, Element) and isinstance(old_child, Element):
635 | self.window.evaluate_js(f'{self.js_path}.replaceChild({new_child.js_path}, {old_child.js_path})')
636 | else:
637 | raise TypeError("replaceChild expects Element objects")
638 |
639 | def addEventListener(self, event_type, callback)-> bool:
640 | """Add event listener. Returns success. Example:
641 | >>> element.addEventListener("click",callback_function)
642 | -> True (if successful)"""
643 |
644 | element_id = self.window.evaluate_js(f"""
645 | (function() {{
646 | var el = {self.js_path};
647 | return el ? (el.id || 'anonymous_' + Math.random().toString(36).substr(2, 9)) : null;
648 | }})()
649 | """)
650 |
651 | if not element_id:
652 | print(f"WARNING: Could not get ID for element: {self.js_path}")
653 | return False
654 |
655 | # Get the PyContext from the window
656 | context = None
657 | if hasattr(self.window, '_py_context'):
658 | context = self.window._py_context
659 |
660 | if not context:
661 | # If no context found, create a temporary one just for this handler
662 | context = PositronContext(self.window)
663 | self.window._py_context = context
664 |
665 | # Register the event handler with the context
666 | handler_key = context.register_event_handler(element_id, event_type, callback)
667 |
668 | # Create global event handler if not already created
669 | if not hasattr(self.window, 'handle_py_event'):
670 | def handle_py_event(element_id, event_type):
671 | key = f"{element_id}_{event_type}"
672 | if hasattr(self.window, '_py_context') and key in self.window._py_context.event_handlers:
673 | try:
674 | return self.window._py_context.event_handlers[key]()
675 | except Exception as e:
676 | print(f"[ERROR] handling event: {e}")
677 | traceback.print_exc()
678 | return str(e)
679 | print(f"WARNING: No handler found for {key}")
680 | return False
681 |
682 | self.window.handle_py_event = handle_py_event
683 | self.window.expose(handle_py_event)
684 |
685 | # Add the event listener in JavaScript
686 | js_code = f"""
687 | (function() {{
688 | var element = {self.js_path};
689 | if (!element) return false;
690 |
691 | element.addEventListener("{event_type}", function(event) {{
692 | console.log("Event triggered: {event_type} on {element_id}");
693 | window.pywebview.api.handle_py_event("{element_id}", "{event_type}");
694 | }});
695 | return true;
696 | }})();
697 | """
698 |
699 | success = self.window.evaluate_js(js_code)
700 |
701 | return success
702 | class Style:
703 | def __init__(self, window, js_path):
704 | self.window = window
705 | self.js_path = js_path
706 |
707 | def __setattr__(self, name, value):
708 | if name in ['window', 'js_path']:
709 | super().__setattr__(name, value)
710 | else:
711 | self.window.evaluate_js(f'{self.js_path}.{name} = {json.dumps(value)}')
712 | def __getattr__(self, name):
713 | return self.window.evaluate_js(f'{self.js_path}.{name}')
714 |
715 | class ElementList:
716 | def __init__(self, window, js_path):
717 | self.window = window
718 | self.js_path = js_path
719 | self.length = self.window.evaluate_js(f'{self.js_path}.length') or 0
720 | def __getitem__(self, index):
721 | if 0 <= index < self.length:
722 | return Element(self.window, f'{self.js_path}[{index}]')
723 | raise IndexError("ElementList index out of range")
724 | def __len__(self):
725 | return self.length
726 |
727 | def __iter__(self):
728 | for i in range(self.length):
729 | yield self[i]
730 | yield self[i]
731 |
732 | class Console:
733 | """Console object for debugging and logging"""
734 | def __init__(self, window):
735 | self.window = window
736 |
737 | def assert_(self, assertion, *message):
738 | """Writes an error message to the console if a assertion is false"""
739 | if not assertion:
740 | msg = ' '.join(str(arg) for arg in message) if message else 'Assertion failed'
741 | self.window.evaluate_js(f'console.assert({json.dumps(assertion)}, {json.dumps(msg)})')
742 |
743 | def clear(self):
744 | """Clears the console"""
745 | self.window.evaluate_js('console.clear()')
746 |
747 | def count(self, label='default'):
748 | """Logs the number of times that this particular call to count() has been called"""
749 | self.window.evaluate_js(f'console.count({json.dumps(label)})')
750 |
751 | def error(self, *args):
752 | """Outputs an error message to the console"""
753 | message = ' '.join(str(arg) for arg in args)
754 | self.window.evaluate_js(f'console.error({json.dumps(message)})')
755 |
756 | def group(self, *args):
757 | """Creates a new inline group in the console"""
758 | message = ' '.join(str(arg) for arg in args) if args else ''
759 | self.window.evaluate_js(f'console.group({json.dumps(message)})')
760 |
761 | def groupCollapsed(self, *args):
762 | """Creates a new inline group in the console, but collapsed"""
763 | message = ' '.join(str(arg) for arg in args) if args else ''
764 | self.window.evaluate_js(f'console.groupCollapsed({json.dumps(message)})')
765 |
766 | def groupEnd(self):
767 | """Exits the current inline group in the console"""
768 | self.window.evaluate_js('console.groupEnd()')
769 |
770 | def info(self, *args):
771 | """Outputs an informational message to the console"""
772 | message = ' '.join(str(arg) for arg in args)
773 | self.window.evaluate_js(f'console.info({json.dumps(message)})')
774 |
775 | def log(self, *args):
776 | """Outputs a message to the console"""
777 | message = ' '.join(str(arg) for arg in args)
778 | self.window.evaluate_js(f'console.log({json.dumps(message)})')
779 |
780 | def table(self, data):
781 | """Displays tabular data as a table"""
782 | self.window.evaluate_js(f'console.table({json.dumps(data)})')
783 |
784 | def time(self, label='default'):
785 | """Starts a timer"""
786 | self.window.evaluate_js(f'console.time({json.dumps(label)})')
787 |
788 | def timeEnd(self, label='default'):
789 | """Stops a timer that was previously started by console.time()"""
790 | self.window.evaluate_js(f'console.timeEnd({json.dumps(label)})')
791 |
792 | def trace(self, *args):
793 | """Outputs a stack trace to the console"""
794 | message = ' '.join(str(arg) for arg in args) if args else ''
795 | self.window.evaluate_js(f'console.trace({json.dumps(message)})')
796 |
797 | def warn(self, *args):
798 | """Outputs a warning message to the console"""
799 | message = ' '.join(str(arg) for arg in args)
800 | self.window.evaluate_js(f'console.warn({json.dumps(message)})')
801 |
802 | class History:
803 | """History object for navigation"""
804 | def __init__(self, window):
805 | self.window = window
806 |
807 | @property
808 | def length(self) -> int:
809 | """Returns the number of URLs (pages) in the history list"""
810 | return self.window.evaluate_js('history.length') or 0
811 |
812 | def back(self):
813 | """Loads the previous URL (page) in the history list"""
814 | self.window.evaluate_js('history.back()')
815 |
816 | def forward(self):
817 | """Loads the next URL (page) in the history list"""
818 | self.window.evaluate_js('history.forward()')
819 |
820 | def go(self, number: int):
821 | """Loads a specific URL (page) from the history list"""
822 | self.window.evaluate_js(f'history.go({number})')
823 |
824 | class Location:
825 | """Location object for URL manipulation"""
826 | def __init__(self, window):
827 | self.window = window
828 |
829 | @property
830 | def hash(self) -> str:
831 | """Sets or returns the anchor part (#) of a URL"""
832 | return self.window.evaluate_js('location.hash') or ''
833 |
834 | @hash.setter
835 | def hash(self, value: str):
836 | """Sets the anchor part (#) of a URL"""
837 | self.window.evaluate_js(f'location.hash = {json.dumps(value)}')
838 |
839 | @property
840 | def host(self) -> str:
841 | """Sets or returns the hostname and port number of a URL"""
842 | return self.window.evaluate_js('location.host') or ''
843 |
844 | @host.setter
845 | def host(self, value: str):
846 | """Sets the hostname and port number of a URL"""
847 | self.window.evaluate_js(f'location.host = {json.dumps(value)}')
848 |
849 | @property
850 | def hostname(self) -> str:
851 | """Sets or returns the hostname of a URL"""
852 | return self.window.evaluate_js('location.hostname') or ''
853 |
854 | @hostname.setter
855 | def hostname(self, value: str):
856 | """Sets the hostname of a URL"""
857 | self.window.evaluate_js(f'location.hostname = {json.dumps(value)}')
858 |
859 | @property
860 | def href(self) -> str:
861 | """Sets or returns the entire URL"""
862 | return self.window.evaluate_js('location.href') or ''
863 |
864 | @href.setter
865 | def href(self, value: str):
866 | """Sets the entire URL"""
867 | self.window.evaluate_js(f'location.href = {json.dumps(value)}')
868 |
869 | @property
870 | def origin(self) -> str:
871 | """Returns the protocol, hostname and port number of a URL"""
872 | return self.window.evaluate_js('location.origin') or ''
873 |
874 | @property
875 | def pathname(self) -> str:
876 | """Sets or returns the path name of a URL"""
877 | return self.window.evaluate_js('location.pathname') or ''
878 |
879 | @pathname.setter
880 | def pathname(self, value: str):
881 | """Sets the path name of a URL"""
882 | self.window.evaluate_js(f'location.pathname = {json.dumps(value)}')
883 |
884 | @property
885 | def port(self) -> str:
886 | """Sets or returns the port number of a URL"""
887 | return self.window.evaluate_js('location.port') or ''
888 |
889 | @port.setter
890 | def port(self, value: str):
891 | """Sets the port number of a URL"""
892 | self.window.evaluate_js(f'location.port = {json.dumps(value)}')
893 |
894 | @property
895 | def protocol(self) -> str:
896 | """Sets or returns the protocol of a URL"""
897 | return self.window.evaluate_js('location.protocol') or ''
898 |
899 | @protocol.setter
900 | def protocol(self, value: str):
901 | """Sets the protocol of a URL"""
902 | self.window.evaluate_js(f'location.protocol = {json.dumps(value)}')
903 |
904 | @property
905 | def search(self) -> str:
906 | """Sets or returns the querystring part of a URL"""
907 | return self.window.evaluate_js('location.search') or ''
908 |
909 | @search.setter
910 | def search(self, value: str):
911 | """Sets the querystring part of a URL"""
912 | self.window.evaluate_js(f'location.search = {json.dumps(value)}')
913 |
914 | def assign(self, url: str):
915 | """Loads a new document"""
916 | self.window.evaluate_js(f'location.assign({json.dumps(url)})')
917 |
918 | def reload(self, force_reload: bool = False):
919 | """Reloads the current document"""
920 | self.window.evaluate_js(f'location.reload({json.dumps(force_reload)})')
921 |
922 | def replace(self, url: str):
923 | """Replaces the current document with a new one"""
924 | self.window.evaluate_js(f'location.replace({json.dumps(url)})')
925 |
926 | class Navigator:
927 | """Navigator object for browser information"""
928 | def __init__(self, window):
929 | self.window = window
930 |
931 | @property
932 | def appCodeName(self) -> str:
933 | """Returns the application code name of the browser"""
934 | return self.window.evaluate_js('navigator.appCodeName') or ''
935 |
936 | @property
937 | def appName(self) -> str:
938 | """Returns the application name of the browser"""
939 | return self.window.evaluate_js('navigator.appName') or ''
940 |
941 | @property
942 | def appVersion(self) -> str:
943 | """Returns the version information of the browser"""
944 | return self.window.evaluate_js('navigator.appVersion') or ''
945 |
946 | @property
947 | def cookieEnabled(self) -> bool:
948 | """Returns true if browser cookies are enabled"""
949 | return self.window.evaluate_js('navigator.cookieEnabled') or False
950 |
951 | @property
952 | def geolocation(self):
953 | """Returns a geolocation object for the user's location"""
954 | return self.window.evaluate_js('navigator.geolocation')
955 |
956 | @property
957 | def language(self) -> str:
958 | """Returns browser language"""
959 | return self.window.evaluate_js('navigator.language') or ''
960 |
961 | @property
962 | def onLine(self) -> bool:
963 | """Returns true if the browser is online"""
964 | return self.window.evaluate_js('navigator.onLine') or False
965 |
966 | @property
967 | def platform(self) -> str:
968 | """Returns the platform of the browser"""
969 | return self.window.evaluate_js('navigator.platform') or ''
970 |
971 | @property
972 | def product(self) -> str:
973 | """Returns the product name of the browser"""
974 | return self.window.evaluate_js('navigator.product') or ''
975 |
976 | @property
977 | def userAgent(self) -> str:
978 | """Returns browser user-agent header"""
979 | return self.window.evaluate_js('navigator.userAgent') or ''
980 |
981 | def javaEnabled(self) -> bool:
982 | """Returns whether Java is enabled in the browser"""
983 | return self.window.evaluate_js('navigator.javaEnabled()') or False
984 |
985 | class Screen:
986 | """Screen object for screen information"""
987 | def __init__(self, window):
988 | self.window = window
989 |
990 | @property
991 | def availHeight(self) -> int:
992 | """Returns the height of the screen (excluding the Windows Taskbar)"""
993 | return self.window.evaluate_js('screen.availHeight') or 0
994 |
995 | @property
996 | def availWidth(self) -> int:
997 | """Returns the width of the screen (excluding the Windows Taskbar)"""
998 | return self.window.evaluate_js('screen.availWidth') or 0
999 |
1000 | @property
1001 | def colorDepth(self) -> int:
1002 | """Returns the bit depth of the color palette for displaying images"""
1003 | return self.window.evaluate_js('screen.colorDepth') or 0
1004 |
1005 | @property
1006 | def height(self) -> int:
1007 | """Returns the total height of the screen"""
1008 | return self.window.evaluate_js('screen.height') or 0
1009 |
1010 | @property
1011 | def pixelDepth(self) -> int:
1012 | """Returns the color resolution (in bits per pixel) of the screen"""
1013 | return self.window.evaluate_js('screen.pixelDepth') or 0
1014 |
1015 | @property
1016 | def width(self) -> int:
1017 | """Returns the total width of the screen"""
1018 | return self.window.evaluate_js('screen.width') or 0
1019 |
1020 | class HTMLWindow:
1021 | """HTML Window object providing JavaScript window functionality"""
1022 | def __init__(self, window):
1023 | self.window = window
1024 | self._console = Console(window)
1025 | self._history = History(window)
1026 | self._location = Location(window)
1027 | self._navigator = Navigator(window)
1028 | self._screen = Screen(window)
1029 |
1030 | # Properties
1031 | @property
1032 | def closed(self) -> bool:
1033 | """Returns a boolean true if a window is closed"""
1034 | return self.window.evaluate_js('window.closed') or False
1035 |
1036 | @property
1037 | def console(self) -> Console:
1038 | """Returns the Console Object for the window"""
1039 | return self._console
1040 |
1041 | @property
1042 | def document(self):
1043 | """Returns the Document object for the window"""
1044 | return Document(self.window)
1045 |
1046 | @property
1047 | def frameElement(self):
1048 | """Returns the frame in which the window runs"""
1049 | return self.window.evaluate_js('window.frameElement')
1050 |
1051 | @property
1052 | def frames(self):
1053 | """Returns all window objects running in the window"""
1054 | return self.window.evaluate_js('window.frames')
1055 |
1056 | @property
1057 | def history(self) -> History:
1058 | """Returns the History object for the window"""
1059 | return self._history
1060 |
1061 | @property
1062 | def innerHeight(self) -> int:
1063 | """Returns the height of the window's content area (viewport) including scrollbars"""
1064 | return self.window.evaluate_js('window.innerHeight') or 0
1065 |
1066 | @property
1067 | def innerWidth(self) -> int:
1068 | """Returns the width of a window's content area (viewport) including scrollbars"""
1069 | return self.window.evaluate_js('window.innerWidth') or 0
1070 |
1071 | @property
1072 | def length(self) -> int:
1073 | """Returns the number of