patch-chat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. patch/__init__.py +20 -0
  2. patch/__main__.py +4 -0
  3. patch/_version.py +24 -0
  4. patch/analytics.py +260 -0
  5. patch/args.py +955 -0
  6. patch/args_formatter.py +228 -0
  7. patch/coders/__init__.py +32 -0
  8. patch/coders/architect_coder.py +47 -0
  9. patch/coders/architect_prompts.py +40 -0
  10. patch/coders/ask_coder.py +9 -0
  11. patch/coders/ask_prompts.py +41 -0
  12. patch/coders/base_coder.py +2548 -0
  13. patch/coders/base_prompts.py +60 -0
  14. patch/coders/chat_chunks.py +64 -0
  15. patch/coders/context_coder.py +53 -0
  16. patch/coders/context_prompts.py +75 -0
  17. patch/coders/edit_apply.py +6 -0
  18. patch/coders/editblock_coder.py +620 -0
  19. patch/coders/editblock_fenced_coder.py +10 -0
  20. patch/coders/editblock_fenced_prompts.py +143 -0
  21. patch/coders/editblock_func_coder.py +141 -0
  22. patch/coders/editblock_func_prompts.py +27 -0
  23. patch/coders/editblock_prompts.py +172 -0
  24. patch/coders/editor_diff_fenced_coder.py +9 -0
  25. patch/coders/editor_diff_fenced_prompts.py +11 -0
  26. patch/coders/editor_editblock_coder.py +8 -0
  27. patch/coders/editor_editblock_prompts.py +18 -0
  28. patch/coders/editor_whole_coder.py +8 -0
  29. patch/coders/editor_whole_prompts.py +10 -0
  30. patch/coders/patch_coder.py +731 -0
  31. patch/coders/patch_prompts.py +159 -0
  32. patch/coders/search_replace.py +757 -0
  33. patch/coders/shell.py +37 -0
  34. patch/coders/single_wholefile_func_coder.py +102 -0
  35. patch/coders/single_wholefile_func_prompts.py +27 -0
  36. patch/coders/udiff_coder.py +419 -0
  37. patch/coders/udiff_prompts.py +113 -0
  38. patch/coders/udiff_simple.py +14 -0
  39. patch/coders/udiff_simple_prompts.py +25 -0
  40. patch/coders/wholefile_coder.py +144 -0
  41. patch/coders/wholefile_func_coder.py +134 -0
  42. patch/coders/wholefile_func_prompts.py +27 -0
  43. patch/coders/wholefile_prompts.py +64 -0
  44. patch/commands.py +1668 -0
  45. patch/copypaste.py +70 -0
  46. patch/deprecated.py +126 -0
  47. patch/diffs.py +128 -0
  48. patch/dump.py +29 -0
  49. patch/editor.py +147 -0
  50. patch/exceptions.py +113 -0
  51. patch/format_settings.py +47 -0
  52. patch/gui.py +545 -0
  53. patch/history.py +143 -0
  54. patch/io.py +1209 -0
  55. patch/linter.py +304 -0
  56. patch/llm.py +47 -0
  57. patch/main.py +1466 -0
  58. patch/mdstream.py +243 -0
  59. patch/models.py +1342 -0
  60. patch/onboarding.py +428 -0
  61. patch/openrouter.py +128 -0
  62. patch/prompts.py +61 -0
  63. patch/queries/tree-sitter-language-pack/README.md +7 -0
  64. patch/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  65. patch/queries/tree-sitter-language-pack/bash-tags.scm +8 -0
  66. patch/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  67. patch/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  68. patch/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
  69. patch/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  70. patch/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  71. patch/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  72. patch/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  73. patch/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  74. patch/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  75. patch/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  76. patch/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  77. patch/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  78. patch/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  79. patch/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  80. patch/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  81. patch/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  82. patch/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
  83. patch/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  84. patch/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  85. patch/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  86. patch/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  87. patch/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  88. patch/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  89. patch/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  90. patch/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  91. patch/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  92. patch/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  93. patch/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  94. patch/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  95. patch/queries/tree-sitter-languages/README.md +24 -0
  96. patch/queries/tree-sitter-languages/bash-tags.scm +8 -0
  97. patch/queries/tree-sitter-languages/c-tags.scm +9 -0
  98. patch/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  99. patch/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  100. patch/queries/tree-sitter-languages/dart-tags.scm +91 -0
  101. patch/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  102. patch/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  103. patch/queries/tree-sitter-languages/elm-tags.scm +19 -0
  104. patch/queries/tree-sitter-languages/fortran-tags.scm +15 -0
  105. patch/queries/tree-sitter-languages/go-tags.scm +30 -0
  106. patch/queries/tree-sitter-languages/haskell-tags.scm +3 -0
  107. patch/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  108. patch/queries/tree-sitter-languages/java-tags.scm +20 -0
  109. patch/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  110. patch/queries/tree-sitter-languages/julia-tags.scm +60 -0
  111. patch/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  112. patch/queries/tree-sitter-languages/matlab-tags.scm +10 -0
  113. patch/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  114. patch/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  115. patch/queries/tree-sitter-languages/php-tags.scm +26 -0
  116. patch/queries/tree-sitter-languages/python-tags.scm +12 -0
  117. patch/queries/tree-sitter-languages/ql-tags.scm +26 -0
  118. patch/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  119. patch/queries/tree-sitter-languages/rust-tags.scm +60 -0
  120. patch/queries/tree-sitter-languages/scala-tags.scm +65 -0
  121. patch/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  122. patch/queries/tree-sitter-languages/zig-tags.scm +3 -0
  123. patch/reasoning_tags.py +82 -0
  124. patch/repo.py +612 -0
  125. patch/repomap.py +859 -0
  126. patch/report.py +200 -0
  127. patch/resources/__init__.py +3 -0
  128. patch/resources/model-metadata.json +715 -0
  129. patch/resources/model-settings.yml +3128 -0
  130. patch/run_cmd.py +146 -0
  131. patch/scrape.py +316 -0
  132. patch/sendchat.py +61 -0
  133. patch/special.py +203 -0
  134. patch/urls.py +17 -0
  135. patch/utils.py +365 -0
  136. patch/versioncheck.py +113 -0
  137. patch/voice.py +200 -0
  138. patch/waiting.py +222 -0
  139. patch/watch.py +318 -0
  140. patch/watch_prompts.py +12 -0
  141. patch_chat-0.1.0.dist-info/METADATA +375 -0
  142. patch_chat-0.1.0.dist-info/RECORD +146 -0
  143. patch_chat-0.1.0.dist-info/WHEEL +5 -0
  144. patch_chat-0.1.0.dist-info/entry_points.txt +2 -0
  145. patch_chat-0.1.0.dist-info/licenses/LICENSE.txt +202 -0
  146. patch_chat-0.1.0.dist-info/top_level.txt +1 -0
patch/special.py ADDED
@@ -0,0 +1,203 @@
1
+ import os
2
+
3
+ ROOT_IMPORTANT_FILES = [
4
+ # Version Control
5
+ ".gitignore",
6
+ ".gitattributes",
7
+ # Documentation
8
+ "README",
9
+ "README.md",
10
+ "README.txt",
11
+ "README.rst",
12
+ "CONTRIBUTING",
13
+ "CONTRIBUTING.md",
14
+ "CONTRIBUTING.txt",
15
+ "CONTRIBUTING.rst",
16
+ "LICENSE",
17
+ "LICENSE.md",
18
+ "LICENSE.txt",
19
+ "CHANGELOG",
20
+ "CHANGELOG.md",
21
+ "CHANGELOG.txt",
22
+ "CHANGELOG.rst",
23
+ "SECURITY",
24
+ "SECURITY.md",
25
+ "SECURITY.txt",
26
+ "CODEOWNERS",
27
+ # Package Management and Dependencies
28
+ "requirements.txt",
29
+ "Pipfile",
30
+ "Pipfile.lock",
31
+ "pyproject.toml",
32
+ "setup.py",
33
+ "setup.cfg",
34
+ "package.json",
35
+ "package-lock.json",
36
+ "yarn.lock",
37
+ "npm-shrinkwrap.json",
38
+ "Gemfile",
39
+ "Gemfile.lock",
40
+ "composer.json",
41
+ "composer.lock",
42
+ "pom.xml",
43
+ "build.gradle",
44
+ "build.gradle.kts",
45
+ "build.sbt",
46
+ "go.mod",
47
+ "go.sum",
48
+ "Cargo.toml",
49
+ "Cargo.lock",
50
+ "mix.exs",
51
+ "rebar.config",
52
+ "project.clj",
53
+ "Podfile",
54
+ "Cartfile",
55
+ "dub.json",
56
+ "dub.sdl",
57
+ # Configuration and Settings
58
+ ".env",
59
+ ".env.example",
60
+ ".editorconfig",
61
+ "tsconfig.json",
62
+ "jsconfig.json",
63
+ ".babelrc",
64
+ "babel.config.js",
65
+ ".eslintrc",
66
+ ".eslintignore",
67
+ ".prettierrc",
68
+ ".stylelintrc",
69
+ "tslint.json",
70
+ ".pylintrc",
71
+ ".flake8",
72
+ ".rubocop.yml",
73
+ ".scalafmt.conf",
74
+ ".dockerignore",
75
+ ".gitpod.yml",
76
+ "sonar-project.properties",
77
+ "renovate.json",
78
+ "dependabot.yml",
79
+ ".pre-commit-config.yaml",
80
+ "mypy.ini",
81
+ "tox.ini",
82
+ ".yamllint",
83
+ "pyrightconfig.json",
84
+ # Build and Compilation
85
+ "webpack.config.js",
86
+ "rollup.config.js",
87
+ "parcel.config.js",
88
+ "gulpfile.js",
89
+ "Gruntfile.js",
90
+ "build.xml",
91
+ "build.boot",
92
+ "project.json",
93
+ "build.cake",
94
+ "MANIFEST.in",
95
+ # Testing
96
+ "pytest.ini",
97
+ "phpunit.xml",
98
+ "karma.conf.js",
99
+ "jest.config.js",
100
+ "cypress.json",
101
+ ".nycrc",
102
+ ".nycrc.json",
103
+ # CI/CD
104
+ ".travis.yml",
105
+ ".gitlab-ci.yml",
106
+ "Jenkinsfile",
107
+ "azure-pipelines.yml",
108
+ "bitbucket-pipelines.yml",
109
+ "appveyor.yml",
110
+ "circle.yml",
111
+ ".circleci/config.yml",
112
+ ".github/dependabot.yml",
113
+ "codecov.yml",
114
+ ".coveragerc",
115
+ # Docker and Containers
116
+ "Dockerfile",
117
+ "docker-compose.yml",
118
+ "docker-compose.override.yml",
119
+ # Cloud and Serverless
120
+ "serverless.yml",
121
+ "firebase.json",
122
+ "now.json",
123
+ "netlify.toml",
124
+ "vercel.json",
125
+ "app.yaml",
126
+ "terraform.tf",
127
+ "main.tf",
128
+ "cloudformation.yaml",
129
+ "cloudformation.json",
130
+ "ansible.cfg",
131
+ "kubernetes.yaml",
132
+ "k8s.yaml",
133
+ # Database
134
+ "schema.sql",
135
+ "liquibase.properties",
136
+ "flyway.conf",
137
+ # Framework-specific
138
+ "next.config.js",
139
+ "nuxt.config.js",
140
+ "vue.config.js",
141
+ "angular.json",
142
+ "gatsby-config.js",
143
+ "gridsome.config.js",
144
+ # API Documentation
145
+ "swagger.yaml",
146
+ "swagger.json",
147
+ "openapi.yaml",
148
+ "openapi.json",
149
+ # Development environment
150
+ ".nvmrc",
151
+ ".ruby-version",
152
+ ".python-version",
153
+ "Vagrantfile",
154
+ # Quality and metrics
155
+ ".codeclimate.yml",
156
+ "codecov.yml",
157
+ # Documentation
158
+ "mkdocs.yml",
159
+ "_config.yml",
160
+ "book.toml",
161
+ "readthedocs.yml",
162
+ ".readthedocs.yaml",
163
+ # Package registries
164
+ ".npmrc",
165
+ ".yarnrc",
166
+ # Linting and formatting
167
+ ".isort.cfg",
168
+ ".markdownlint.json",
169
+ ".markdownlint.yaml",
170
+ # Security
171
+ ".bandit",
172
+ ".secrets.baseline",
173
+ # Misc
174
+ ".pypirc",
175
+ ".gitkeep",
176
+ ".npmignore",
177
+ ]
178
+
179
+
180
+ # Normalize the lists once
181
+ NORMALIZED_ROOT_IMPORTANT_FILES = set(os.path.normpath(path) for path in ROOT_IMPORTANT_FILES)
182
+
183
+
184
+ def is_important(file_path):
185
+ file_name = os.path.basename(file_path)
186
+ dir_name = os.path.normpath(os.path.dirname(file_path))
187
+ normalized_path = os.path.normpath(file_path)
188
+
189
+ # Check for GitHub Actions workflow files
190
+ if dir_name == os.path.normpath(".github/workflows") and file_name.endswith(".yml"):
191
+ return True
192
+
193
+ return normalized_path in NORMALIZED_ROOT_IMPORTANT_FILES
194
+
195
+
196
+ def filter_important_files(file_paths):
197
+ """
198
+ Filter a list of file paths to return only those that are commonly important in codebases.
199
+
200
+ :param file_paths: List of file paths to check
201
+ :return: List of file paths that match important file patterns
202
+ """
203
+ return list(filter(is_important, file_paths))
patch/urls.py ADDED
@@ -0,0 +1,17 @@
1
+ website = "https://getpatch.dev/"
2
+ documentation = "https://github.com/PierrunoYT/patch#readme"
3
+ add_all_files = documentation
4
+ edit_errors = "https://github.com/PierrunoYT/patch/issues/new"
5
+ git = documentation
6
+ enable_playwright = documentation
7
+ model_warnings = documentation
8
+ token_limits = documentation
9
+ llms = documentation
10
+ large_repos = documentation
11
+ github_issues = "https://github.com/PierrunoYT/patch/issues/new"
12
+ git_index_version = documentation
13
+ install_properly = documentation
14
+ analytics = "https://github.com/PierrunoYT/patch#security-boundaries"
15
+ release_notes = "https://github.com/PierrunoYT/patch/blob/main/CHANGELOG.md"
16
+ edit_formats = documentation
17
+ models_and_keys = documentation
patch/utils.py ADDED
@@ -0,0 +1,365 @@
1
+ import os
2
+ import platform
3
+ import subprocess
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ import oslex
9
+
10
+ from patch.dump import dump # noqa: F401
11
+ from patch.waiting import Spinner
12
+
13
+ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp", ".pdf"}
14
+
15
+
16
+ class IgnorantTemporaryDirectory:
17
+ def __init__(self):
18
+ if sys.version_info >= (3, 10):
19
+ self.temp_dir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
20
+ else:
21
+ self.temp_dir = tempfile.TemporaryDirectory()
22
+
23
+ def __enter__(self):
24
+ return self.temp_dir.__enter__()
25
+
26
+ def __exit__(self, exc_type, exc_val, exc_tb):
27
+ self.cleanup()
28
+
29
+ def cleanup(self):
30
+ try:
31
+ self.temp_dir.cleanup()
32
+ except (OSError, PermissionError, RecursionError):
33
+ pass # Ignore errors (Windows and potential recursion)
34
+
35
+ def __getattr__(self, item):
36
+ return getattr(self.temp_dir, item)
37
+
38
+
39
+ class ChdirTemporaryDirectory(IgnorantTemporaryDirectory):
40
+ def __init__(self):
41
+ try:
42
+ self.cwd = os.getcwd()
43
+ except FileNotFoundError:
44
+ self.cwd = None
45
+
46
+ super().__init__()
47
+
48
+ def __enter__(self):
49
+ res = super().__enter__()
50
+ os.chdir(Path(self.temp_dir.name).resolve())
51
+ return res
52
+
53
+ def __exit__(self, exc_type, exc_val, exc_tb):
54
+ if self.cwd:
55
+ try:
56
+ os.chdir(self.cwd)
57
+ except FileNotFoundError:
58
+ pass
59
+ super().__exit__(exc_type, exc_val, exc_tb)
60
+
61
+
62
+ class GitTemporaryDirectory(ChdirTemporaryDirectory):
63
+ def __enter__(self):
64
+ dname = super().__enter__()
65
+ self.repo = make_repo(dname)
66
+ return dname
67
+
68
+ def __exit__(self, exc_type, exc_val, exc_tb):
69
+ del self.repo
70
+ super().__exit__(exc_type, exc_val, exc_tb)
71
+
72
+
73
+ def make_repo(path=None):
74
+ import git
75
+
76
+ if not path:
77
+ path = "."
78
+ repo = git.Repo.init(path)
79
+ repo.config_writer().set_value("user", "name", "Test User").release()
80
+ repo.config_writer().set_value("user", "email", "testuser@example.com").release()
81
+
82
+ return repo
83
+
84
+
85
+ def is_image_file(file_name):
86
+ """
87
+ Check if the given file name has an image file extension.
88
+
89
+ :param file_name: The name of the file to check.
90
+ :return: True if the file is an image, False otherwise.
91
+ """
92
+ return Path(file_name).suffix.lower() in IMAGE_EXTENSIONS
93
+
94
+
95
+ def safe_abs_path(res):
96
+ "Gives an abs path, which safely returns a full (not 8.3) windows path"
97
+ try:
98
+ res = Path(res).resolve()
99
+ except (RuntimeError, OSError):
100
+ res = Path(res).absolute()
101
+ return str(res)
102
+
103
+
104
+ def path_is_within(path, root):
105
+ """Is `path` inside `root` after fully resolving both?
106
+
107
+ Resolving both sides is what makes this a real containment check rather
108
+ than a lexical one: it collapses `..` segments and follows symlinks, so a
109
+ traversal path, an absolute path, and a symlink pointing outside the tree
110
+ are all rejected. A string prefix test is not sufficient -- it accepts
111
+ `/work/repo-secret` for the root `/work/repo`.
112
+ """
113
+ try:
114
+ root = Path(root).resolve()
115
+ path = Path(path).resolve()
116
+ except (RuntimeError, OSError):
117
+ return False
118
+
119
+ return path == root or root in path.parents
120
+
121
+
122
+ def format_content(role, content):
123
+ formatted_lines = []
124
+ for line in content.splitlines():
125
+ formatted_lines.append(f"{role} {line}")
126
+ return "\n".join(formatted_lines)
127
+
128
+
129
+ def format_messages(messages, title=None):
130
+ output = []
131
+ if title:
132
+ output.append(f"{title.upper()} {'*' * 50}")
133
+
134
+ for msg in messages:
135
+ output.append("-------")
136
+ role = msg["role"].upper()
137
+ content = msg.get("content")
138
+ if isinstance(content, list): # Handle list content (e.g., image messages)
139
+ for item in content:
140
+ if isinstance(item, dict):
141
+ for key, value in item.items():
142
+ if isinstance(value, dict) and "url" in value:
143
+ output.append(f"{role} {key.capitalize()} URL: {value['url']}")
144
+ else:
145
+ output.append(f"{role} {key}: {value}")
146
+ else:
147
+ output.append(f"{role} {item}")
148
+ elif isinstance(content, str): # Handle string content
149
+ output.append(format_content(role, content))
150
+ function_call = msg.get("function_call")
151
+ if function_call:
152
+ output.append(f"{role} Function Call: {function_call}")
153
+
154
+ return "\n".join(output)
155
+
156
+
157
+ def show_messages(messages, title=None, functions=None):
158
+ formatted_output = format_messages(messages, title)
159
+ print(formatted_output)
160
+
161
+ if functions:
162
+ dump(functions)
163
+
164
+
165
+ def split_chat_history_markdown(text, include_tool=False):
166
+ messages = []
167
+ user = []
168
+ assistant = []
169
+ tool = []
170
+ lines = text.splitlines(keepends=True)
171
+
172
+ def append_msg(role, lines):
173
+ lines = "".join(lines)
174
+ if lines.strip():
175
+ messages.append(dict(role=role, content=lines))
176
+
177
+ for line in lines:
178
+ if line.startswith("# "):
179
+ continue
180
+ if line.startswith("> "):
181
+ append_msg("assistant", assistant)
182
+ assistant = []
183
+ append_msg("user", user)
184
+ user = []
185
+ tool.append(line[2:])
186
+ continue
187
+ # if line.startswith("#### /"):
188
+ # continue
189
+
190
+ if line.startswith("#### "):
191
+ append_msg("assistant", assistant)
192
+ assistant = []
193
+ append_msg("tool", tool)
194
+ tool = []
195
+
196
+ content = line[5:]
197
+ user.append(content)
198
+ continue
199
+
200
+ append_msg("user", user)
201
+ user = []
202
+ append_msg("tool", tool)
203
+ tool = []
204
+
205
+ assistant.append(line)
206
+
207
+ append_msg("assistant", assistant)
208
+ append_msg("user", user)
209
+
210
+ if not include_tool:
211
+ messages = [m for m in messages if m["role"] != "tool"]
212
+
213
+ return messages
214
+
215
+
216
+ def get_pip_install(args):
217
+ cmd = [
218
+ sys.executable,
219
+ "-m",
220
+ "pip",
221
+ "install",
222
+ "--upgrade",
223
+ "--upgrade-strategy",
224
+ "only-if-needed",
225
+ ]
226
+ cmd += args
227
+ return cmd
228
+
229
+
230
+ def run_install(cmd):
231
+ print()
232
+ print("Installing:", printable_shell_command(cmd))
233
+
234
+ # First ensure pip is available
235
+ ensurepip_cmd = [sys.executable, "-m", "ensurepip", "--upgrade"]
236
+ try:
237
+ subprocess.run(ensurepip_cmd, capture_output=True, check=False)
238
+ except Exception:
239
+ pass # Continue even if ensurepip fails
240
+
241
+ try:
242
+ output = []
243
+ process = subprocess.Popen(
244
+ cmd,
245
+ stdout=subprocess.PIPE,
246
+ stderr=subprocess.STDOUT,
247
+ text=True,
248
+ bufsize=1,
249
+ universal_newlines=True,
250
+ encoding=sys.stdout.encoding,
251
+ errors="replace",
252
+ )
253
+ spinner = Spinner("Installing...")
254
+
255
+ while True:
256
+ char = process.stdout.read(1)
257
+ if not char:
258
+ break
259
+
260
+ output.append(char)
261
+ spinner.step()
262
+
263
+ spinner.end()
264
+ return_code = process.wait()
265
+ output = "".join(output)
266
+
267
+ if return_code == 0:
268
+ print("Installation complete.")
269
+ print()
270
+ return True, output
271
+
272
+ except subprocess.CalledProcessError as e:
273
+ print(f"\nError running pip install: {e}")
274
+
275
+ print("\nInstallation failed.\n")
276
+
277
+ return False, output
278
+
279
+
280
+ def find_common_root(abs_fnames):
281
+ try:
282
+ if len(abs_fnames) == 1:
283
+ return safe_abs_path(os.path.dirname(list(abs_fnames)[0]))
284
+ elif abs_fnames:
285
+ return safe_abs_path(os.path.commonpath(list(abs_fnames)))
286
+ except OSError:
287
+ pass
288
+
289
+ try:
290
+ return safe_abs_path(os.getcwd())
291
+ except FileNotFoundError:
292
+ # Fallback if cwd is deleted
293
+ return "."
294
+
295
+
296
+ def format_tokens(count):
297
+ if count < 1000:
298
+ return f"{count}"
299
+ elif count < 10000:
300
+ return f"{count / 1000:.1f}k"
301
+ else:
302
+ return f"{round(count / 1000)}k"
303
+
304
+
305
+ def touch_file(fname):
306
+ fname = Path(fname)
307
+ try:
308
+ fname.parent.mkdir(parents=True, exist_ok=True)
309
+ fname.touch()
310
+ return True
311
+ except OSError:
312
+ return False
313
+
314
+
315
+ def check_pip_install_extra(io, module, prompt, pip_install_cmd, self_update=False):
316
+ if module:
317
+ try:
318
+ __import__(module)
319
+ return True
320
+ except (ImportError, ModuleNotFoundError, RuntimeError):
321
+ pass
322
+
323
+ cmd = get_pip_install(pip_install_cmd)
324
+
325
+ if prompt:
326
+ io.tool_warning(prompt)
327
+
328
+ if self_update and platform.system() == "Windows":
329
+ io.tool_output("Run this command to update:")
330
+ print()
331
+ print(printable_shell_command(cmd)) # plain print so it doesn't line-wrap
332
+ return
333
+
334
+ if not io.confirm_ask("Run pip install?", default="y", subject=printable_shell_command(cmd)):
335
+ return
336
+
337
+ success, output = run_install(cmd)
338
+ if success:
339
+ if not module:
340
+ return True
341
+ try:
342
+ __import__(module)
343
+ return True
344
+ except (ImportError, ModuleNotFoundError, RuntimeError) as err:
345
+ io.tool_error(str(err))
346
+ pass
347
+
348
+ io.tool_error(output)
349
+
350
+ print()
351
+ print("Install failed, try running this command manually:")
352
+ print(printable_shell_command(cmd))
353
+
354
+
355
+ def printable_shell_command(cmd_list):
356
+ """
357
+ Convert a list of command arguments to a properly shell-escaped string.
358
+
359
+ Args:
360
+ cmd_list (list): List of command arguments.
361
+
362
+ Returns:
363
+ str: Shell-escaped command string.
364
+ """
365
+ return oslex.join(cmd_list)
patch/versioncheck.py ADDED
@@ -0,0 +1,113 @@
1
+ import os
2
+ import sys
3
+ import time
4
+ from pathlib import Path
5
+
6
+ import packaging.version
7
+
8
+ import patch
9
+ from patch import utils
10
+ from patch.dump import dump # noqa: F401
11
+
12
+ VERSION_CHECK_FNAME = Path.home() / ".patch" / "caches" / "versioncheck"
13
+
14
+
15
+ def install_from_main_branch(io):
16
+ """
17
+ Install the latest version of Patch from the main branch of the GitHub repository.
18
+ """
19
+
20
+ return utils.check_pip_install_extra(
21
+ io,
22
+ None,
23
+ "Install the development version of Patch from the main branch?",
24
+ ["git+https://github.com/PierrunoYT/patch.git"],
25
+ self_update=True,
26
+ )
27
+
28
+
29
+ def install_upgrade(io, latest_version=None):
30
+ """
31
+ Install the latest version of Patch from PyPI.
32
+ """
33
+
34
+ if latest_version:
35
+ new_ver_text = f"Newer Patch version v{latest_version} is available."
36
+ else:
37
+ new_ver_text = "Install latest version of Patch?"
38
+
39
+ docker_image = os.environ.get("PATCH_DOCKER_IMAGE")
40
+ if docker_image:
41
+ text = f"""
42
+ {new_ver_text} To upgrade, run:
43
+
44
+ docker pull {docker_image}
45
+ """
46
+ io.tool_warning(text)
47
+ return True
48
+
49
+ success = utils.check_pip_install_extra(
50
+ io,
51
+ None,
52
+ new_ver_text,
53
+ ["patch-chat"],
54
+ self_update=True,
55
+ )
56
+
57
+ if success:
58
+ io.tool_output("Re-run patch to use new version.")
59
+ sys.exit()
60
+
61
+ return
62
+
63
+
64
+ def check_version(io, just_check=False, verbose=False):
65
+ if not just_check and VERSION_CHECK_FNAME.exists():
66
+ day = 60 * 60 * 24
67
+ since = time.time() - os.path.getmtime(VERSION_CHECK_FNAME)
68
+ if 0 < since < day:
69
+ if verbose:
70
+ hours = since / 60 / 60
71
+ io.tool_output(f"Too soon to check version: {hours:.1f} hours")
72
+ return
73
+
74
+ # To keep startup fast, avoid importing this unless needed
75
+ import requests
76
+
77
+ try:
78
+ response = requests.get("https://pypi.org/pypi/patch-chat/json", timeout=(5, 15))
79
+ data = response.json()
80
+ latest_version = data["info"]["version"]
81
+ current_version = patch.__version__
82
+
83
+ if just_check or verbose:
84
+ io.tool_output(f"Current version: {current_version}")
85
+ io.tool_output(f"Latest version: {latest_version}")
86
+
87
+ is_update_available = packaging.version.parse(latest_version) > packaging.version.parse(
88
+ current_version
89
+ )
90
+ except Exception as err:
91
+ io.tool_error(f"Error checking pypi for new version: {err}")
92
+ return False
93
+ finally:
94
+ VERSION_CHECK_FNAME.parent.mkdir(parents=True, exist_ok=True)
95
+ VERSION_CHECK_FNAME.touch()
96
+
97
+ ###
98
+ # is_update_available = True
99
+
100
+ if just_check or verbose:
101
+ if is_update_available:
102
+ io.tool_output("Update available")
103
+ else:
104
+ io.tool_output("No update available")
105
+
106
+ if just_check:
107
+ return is_update_available
108
+
109
+ if not is_update_available:
110
+ return False
111
+
112
+ install_upgrade(io, latest_version)
113
+ return True