src/eric7/Utilities/__init__.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9167
2d2b9a26e904
child 9214
bd28e56047d7
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Package implementing various functions/classes needed everywhere within eric.
8 """
9
10 import codecs
11 import contextlib
12 import ctypes
13 import fnmatch
14 import functools
15 import getpass
16 import glob
17 import os
18 import re
19 import shlex
20 import subprocess # secok
21 import sys
22
23
24 def __showwarning(message, category, filename, lineno, file=None, line=""):
25 """
26 Module function to raise a SyntaxError for a SyntaxWarning.
27
28 @param message warning object
29 @param category type object of the warning
30 @param filename name of the file causing the warning (string)
31 @param lineno line number causing the warning (integer)
32 @param file file to write the warning message to (ignored)
33 @param line line causing the warning (ignored)
34 @raise err exception of type SyntaxError
35 """
36 if category is SyntaxWarning:
37 err = SyntaxError(str(message))
38 err.filename = filename
39 err.lineno = lineno
40 raise err
41
42 import warnings
43 warnings.showwarning = __showwarning
44
45 from codecs import BOM_UTF8, BOM_UTF16, BOM_UTF32
46
47 from PyQt6.QtCore import (
48 qVersion, PYQT_VERSION_STR, QDir, QProcess, QByteArray, QCoreApplication,
49 QCryptographicHash
50 )
51 from PyQt6.Qsci import QSCINTILLA_VERSION_STR, QsciScintilla
52
53 # import these methods into the Utilities namespace
54 from Globals import ( # __IGNORE_WARNING__
55 isWindowsPlatform, isLinuxPlatform, isMacPlatform, desktopName,
56 sessionType, getConfigDir, setConfigDir, getPythonLibraryDirectory,
57 getPyQt6ModulesDirectory, getQtBinariesPath, getPyQtToolsPath,
58 qVersionTuple, getPythonExecutable
59 )
60
61 from EricWidgets.EricApplication import ericApp
62
63 from UI.Info import Program, Version
64
65 import Preferences
66 from Plugins.CheckerPlugins.SyntaxChecker.SyntaxCheck import (
67 # __IGNORE_WARNING__
68 normalizeCode)
69
70 from eric7config import getConfig
71
72 configDir = None
73
74 codingBytes_regexps = [
75 (5, re.compile(br'''coding[:=]\s*([-\w_.]+)''')),
76 (1, re.compile(br'''<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>''')),
77 ]
78 coding_regexps = [
79 (5, re.compile(r'''coding[:=]\s*([-\w_.]+)''')),
80 (1, re.compile(r'''<\?xml.*\bencoding\s*=\s*['"]([-\w_.]+)['"]\?>''')),
81 ]
82
83 supportedCodecs = [
84 'utf-8',
85
86 'iso-8859-1', 'iso-8859-2', 'iso-8859-3',
87 'iso-8859-4', 'iso-8859-5', 'iso-8859-6', 'iso-8859-7',
88 'iso-8859-8', 'iso-8859-9', 'iso-8859-10', 'iso-8859-11',
89 'iso-8859-13', 'iso-8859-14', 'iso-8859-15', 'iso-8859-16',
90 'latin-1',
91
92 'koi8-r', 'koi8-t', 'koi8-u',
93
94 'utf-7',
95 'utf-16', 'utf-16-be', 'utf-16-le',
96 'utf-32', 'utf-32-be', 'utf-32-le',
97
98 'cp037', 'cp273', 'cp424', 'cp437', 'cp500', 'cp720',
99 'cp737', 'cp775', 'cp850', 'cp852', 'cp855', 'cp856',
100 'cp857', 'cp858', 'cp860', 'cp861', 'cp862', 'cp863',
101 'cp864', 'cp865', 'cp866', 'cp869', 'cp874', 'cp875',
102 'cp932', 'cp949', 'cp950', 'cp1006', 'cp1026', 'cp1125',
103 'cp1140',
104
105 'windows-1250', 'windows-1251', 'windows-1252', 'windows-1253',
106 'windows-1254', 'windows-1255', 'windows-1256', 'windows-1257',
107 'windows-1258',
108
109 'gb2312', 'hz', 'gb18030', 'gbk',
110
111 'iso-2022-jp', 'iso-2022-jp-1', 'iso-2022-jp-2', 'iso-2022-jp-2004',
112 'iso-2022-jp-3', 'iso-2022-jp-ext', 'iso-2022-kr',
113
114 'mac-cyrillic', 'mac-greek', 'mac-iceland', 'mac-latin2',
115 'mac-roman', 'mac-turkish',
116
117 'ascii',
118 'big5-tw', 'big5-hkscs',
119 ]
120
121
122 class CodingError(Exception):
123 """
124 Class implementing an exception, which is raised, if a given coding is
125 incorrect.
126 """
127 def __init__(self, coding):
128 """
129 Constructor
130
131 @param coding coding to include in the message (string)
132 """
133 self.errorMessage = QCoreApplication.translate(
134 "CodingError",
135 "The coding '{0}' is wrong for the given text.").format(coding)
136
137 def __repr__(self):
138 """
139 Special method returning a representation of the exception.
140
141 @return string representing the error message
142 """
143 return str(self.errorMessage)
144
145 def __str__(self):
146 """
147 Special method returning a string representation of the exception.
148
149 @return string representing the error message
150 """
151 return str(self.errorMessage)
152
153
154 def get_codingBytes(text):
155 """
156 Function to get the coding of a bytes text.
157
158 @param text bytes text to inspect (bytes)
159 @return coding string
160 """
161 lines = text.splitlines()
162 for coding in codingBytes_regexps:
163 coding_re = coding[1]
164 head = lines[:coding[0]]
165 for line in head:
166 m = coding_re.search(line)
167 if m:
168 return str(m.group(1), "ascii").lower()
169 return None
170
171
172 def get_coding(text):
173 """
174 Function to get the coding of a text.
175
176 @param text text to inspect (string)
177 @return coding string
178 """
179 lines = text.splitlines()
180 for coding in coding_regexps:
181 coding_re = coding[1]
182 head = lines[:coding[0]]
183 for line in head:
184 m = coding_re.search(line)
185 if m:
186 return m.group(1).lower()
187 return None
188
189
190 def readEncodedFile(filename):
191 """
192 Function to read a file and decode its contents into proper text.
193
194 @param filename name of the file to read (string)
195 @return tuple of decoded text and encoding (string, string)
196 """
197 with open(filename, "rb") as f:
198 text = f.read()
199 return decode(text)
200
201
202 def readEncodedFileWithHash(filename):
203 """
204 Function to read a file, calculate a hash value and decode its contents
205 into proper text.
206
207 @param filename name of the file to read (string)
208 @return tuple of decoded text, encoding and hash value (string, string,
209 string)
210 """
211 with open(filename, "rb") as f:
212 text = f.read()
213 hashStr = str(QCryptographicHash.hash(
214 QByteArray(text), QCryptographicHash.Algorithm.Md5).toHex(),
215 encoding="ASCII")
216 return decode(text) + (hashStr, )
217
218
219 def decode(text):
220 """
221 Function to decode some byte text into a string.
222
223 @param text byte text to decode (bytes)
224 @return tuple of decoded text and encoding (string, string)
225 """
226 with contextlib.suppress(UnicodeError, LookupError):
227 if text.startswith(BOM_UTF8):
228 # UTF-8 with BOM
229 return str(text[len(BOM_UTF8):], 'utf-8'), 'utf-8-bom'
230 elif text.startswith(BOM_UTF16):
231 # UTF-16 with BOM
232 return str(text[len(BOM_UTF16):], 'utf-16'), 'utf-16'
233 elif text.startswith(BOM_UTF32):
234 # UTF-32 with BOM
235 return str(text[len(BOM_UTF32):], 'utf-32'), 'utf-32'
236 coding = get_codingBytes(text)
237 if coding:
238 return str(text, coding), coding
239
240 # Assume UTF-8
241 with contextlib.suppress(UnicodeError, LookupError):
242 return str(text, 'utf-8'), 'utf-8-guessed'
243
244 guess = None
245 if Preferences.getEditor("AdvancedEncodingDetection"):
246 # Try the universal character encoding detector
247 try:
248 import chardet
249 guess = chardet.detect(text)
250 if (
251 guess and
252 guess['confidence'] > 0.95 and
253 guess['encoding'] is not None
254 ):
255 codec = guess['encoding'].lower()
256 return str(text, codec), '{0}-guessed'.format(codec)
257 except (UnicodeError, LookupError):
258 pass
259 except ImportError:
260 pass
261
262 # Try default encoding
263 with contextlib.suppress(UnicodeError, LookupError):
264 codec = Preferences.getEditor("DefaultEncoding")
265 return str(text, codec), '{0}-default'.format(codec)
266
267 if (
268 Preferences.getEditor("AdvancedEncodingDetection") and
269 guess and
270 guess['encoding'] is not None
271 ):
272 # Use the guessed one even if confidence level is low
273 with contextlib.suppress(UnicodeError, LookupError):
274 codec = guess['encoding'].lower()
275 return str(text, codec), '{0}-guessed'.format(codec)
276
277 # Assume UTF-8 loosing information
278 return str(text, "utf-8", "ignore"), 'utf-8-ignore'
279
280
281 def readEncodedFileWithEncoding(filename, encoding):
282 """
283 Function to read a file and decode its contents into proper text.
284
285 @param filename name of the file to read (string)
286 @param encoding encoding to be used to read the file (string)
287 @return tuple of decoded text and encoding (string, string)
288 """
289 with open(filename, "rb") as f:
290 text = f.read()
291 if encoding:
292 with contextlib.suppress(UnicodeError, LookupError):
293 return str(text, encoding), '{0}-selected'.format(encoding)
294
295 # Try default encoding
296 with contextlib.suppress(UnicodeError, LookupError):
297 codec = Preferences.getEditor("DefaultEncoding")
298 return str(text, codec), '{0}-default'.format(codec)
299
300 # Assume UTF-8 loosing information
301 return str(text, "utf-8", "ignore"), 'utf-8-ignore'
302 else:
303 return decode(text)
304
305
306 def writeEncodedFile(filename, text, origEncoding, forcedEncoding=""):
307 """
308 Function to write a file with properly encoded text.
309
310 @param filename name of the file to read
311 @type str
312 @param text text to be written
313 @type str
314 @param origEncoding type of the original encoding
315 @type str
316 @param forcedEncoding encoding to be used for writing, if no coding
317 line is present
318 @type str
319 @return encoding used for writing the file
320 @rtype str
321 """
322 etext, encoding = encode(text, origEncoding, forcedEncoding=forcedEncoding)
323
324 with open(filename, "wb") as f:
325 f.write(etext)
326
327 return encoding
328
329
330 def encode(text, origEncoding, forcedEncoding=""):
331 """
332 Function to encode text into a byte text.
333
334 @param text text to be encoded
335 @type str
336 @param origEncoding type of the original encoding
337 @type str
338 @param forcedEncoding encoding to be used for writing, if no coding line
339 is present
340 @type str
341 @return tuple of encoded text and encoding used
342 @rtype tuple of (bytes, str)
343 @exception CodingError raised to indicate an invalid encoding
344 """
345 encoding = None
346 if origEncoding == 'utf-8-bom':
347 etext, encoding = BOM_UTF8 + text.encode("utf-8"), 'utf-8-bom'
348 else:
349 # Try declared coding spec
350 coding = get_coding(text)
351 if coding:
352 try:
353 etext, encoding = text.encode(coding), coding
354 except (UnicodeError, LookupError):
355 # Error: Declared encoding is incorrect
356 raise CodingError(coding)
357 else:
358 if forcedEncoding:
359 with contextlib.suppress(UnicodeError, LookupError):
360 etext, encoding = (
361 text.encode(forcedEncoding), forcedEncoding)
362 # if forced encoding is incorrect, ignore it
363
364 if encoding is None:
365 # Try the original encoding
366 if origEncoding and origEncoding.endswith(
367 ('-selected', '-default', '-guessed', '-ignore')):
368 coding = (
369 origEncoding
370 .replace("-selected", "")
371 .replace("-default", "")
372 .replace("-guessed", "")
373 .replace("-ignore", "")
374 )
375 with contextlib.suppress(UnicodeError, LookupError):
376 etext, encoding = text.encode(coding), coding
377
378 if encoding is None:
379 # Try configured default
380 with contextlib.suppress(UnicodeError, LookupError):
381 codec = Preferences.getEditor("DefaultEncoding")
382 etext, encoding = text.encode(codec), codec
383
384 if encoding is None:
385 # Try saving as ASCII
386 with contextlib.suppress(UnicodeError):
387 etext, encoding = text.encode('ascii'), 'ascii'
388
389 if encoding is None:
390 # Save as UTF-8 without BOM
391 etext, encoding = text.encode('utf-8'), 'utf-8'
392
393 return etext, encoding
394
395
396 def decodeString(text):
397 """
398 Function to decode a string containing Unicode encoded characters.
399
400 @param text text containing encoded chars (string)
401 @return decoded text (string)
402 """
403 buf = b""
404 index = 0
405 while index < len(text):
406 if text[index] == "\\":
407 qb = QByteArray.fromHex(text[index:index + 4].encode())
408 buf += bytes(qb)
409 index += 4
410 else:
411 buf += codecs.encode(text[index], "utf-8")
412 index += 1
413 buf = buf.replace(b"\x00", b"")
414 return decodeBytes(buf)
415
416
417 def decodeBytes(buffer):
418 """
419 Function to decode some byte text into a string.
420
421 @param buffer byte buffer to decode (bytes)
422 @return decoded text (string)
423 """
424 # try UTF with BOM
425 with contextlib.suppress(UnicodeError, LookupError):
426 if buffer.startswith(BOM_UTF8):
427 # UTF-8 with BOM
428 return str(buffer[len(BOM_UTF8):], encoding='utf-8')
429 elif buffer.startswith(BOM_UTF16):
430 # UTF-16 with BOM
431 return str(buffer[len(BOM_UTF16):], encoding='utf-16')
432 elif buffer.startswith(BOM_UTF32):
433 # UTF-32 with BOM
434 return str(buffer[len(BOM_UTF32):], encoding='utf-32')
435
436 # try UTF-8
437 with contextlib.suppress(UnicodeError):
438 return str(buffer, encoding="utf-8")
439
440 # try codec detection
441 try:
442 import chardet
443 guess = chardet.detect(buffer)
444 if guess and guess['encoding'] is not None:
445 codec = guess['encoding'].lower()
446 return str(buffer, encoding=codec)
447 except (UnicodeError, LookupError):
448 pass
449 except ImportError:
450 pass
451
452 return str(buffer, encoding="utf-8", errors="ignore")
453
454
455 def readStringFromStream(stream):
456 """
457 Module function to read a string from the given stream.
458
459 @param stream data stream opened for reading (QDataStream)
460 @return string read from the stream (string)
461 """
462 data = stream.readString()
463 if data is None:
464 data = b""
465 return data.decode('utf-8')
466
467
468 _escape = re.compile("[&<>\"'\u0080-\uffff]")
469
470 _escape_map = {
471 "&": "&amp;",
472 "<": "&lt;",
473 ">": "&gt;",
474 '"': "&quot;",
475 "'": "&#x27;",
476 }
477
478
479 def escape_entities(m, escmap=_escape_map):
480 """
481 Function to encode html entities.
482
483 @param m the match object
484 @param escmap the map of entities to encode
485 @return the converted text (string)
486 """
487 char = m.group()
488 text = escmap.get(char)
489 if text is None:
490 text = "&#{0:d};".format(ord(char))
491 return text
492
493
494 def html_encode(text, pattern=_escape):
495 """
496 Function to correctly encode a text for html.
497
498 @param text text to be encoded (string)
499 @param pattern search pattern for text to be encoded (string)
500 @return the encoded text (string)
501 """
502 if not text:
503 return ""
504 text = pattern.sub(escape_entities, text)
505 return text
506
507 _uescape = re.compile('[\u0080-\uffff]')
508
509
510 def escape_uentities(m):
511 """
512 Function to encode html entities.
513
514 @param m the match object
515 @return the converted text (string)
516 """
517 char = m.group()
518 text = "&#{0:d};".format(ord(char))
519 return text
520
521
522 def html_uencode(text, pattern=_uescape):
523 """
524 Function to correctly encode a unicode text for html.
525
526 @param text text to be encoded (string)
527 @param pattern search pattern for text to be encoded (string)
528 @return the encoded text (string)
529 """
530 if not text:
531 return ""
532 text = pattern.sub(escape_uentities, text)
533 return text
534
535 _uunescape = re.compile(r'&#\d+;')
536
537
538 def unescape_uentities(m):
539 """
540 Function to decode html entities.
541
542 @param m the match object
543 @return the converted text (string)
544 """
545 char = m.group()
546 ordinal = int(char[2:-1])
547 return chr(ordinal)
548
549
550 def html_udecode(text, pattern=_uunescape):
551 """
552 Function to correctly decode a html text to a unicode text.
553
554 @param text text to be decoded (string)
555 @param pattern search pattern for text to be decoded (string)
556 @return the decoded text (string)
557 """
558 if not text:
559 return ""
560 text = pattern.sub(unescape_uentities, text)
561 return text
562
563
564 def convertLineEnds(text, eol):
565 """
566 Function to convert the end of line characters.
567
568 @param text text to be converted (string)
569 @param eol new eol setting (string)
570 @return text with converted eols (string)
571 """
572 if eol == '\r\n':
573 regexp = re.compile(r"""(\r(?!\n)|(?<!\r)\n)""")
574 return regexp.sub(lambda m, eol='\r\n': eol, text)
575 elif eol == '\n':
576 regexp = re.compile(r"""(\r\n|\r)""")
577 return regexp.sub(lambda m, eol='\n': eol, text)
578 elif eol == '\r':
579 regexp = re.compile(r"""(\r\n|\n)""")
580 return regexp.sub(lambda m, eol='\r': eol, text)
581 else:
582 return text
583
584
585 def linesep():
586 """
587 Function to return the line separator used by the editor.
588
589 @return line separator used by the editor (string)
590 """
591 eolMode = Preferences.getEditor("EOLMode")
592 if eolMode == QsciScintilla.EolMode.EolUnix:
593 return "\n"
594 elif eolMode == QsciScintilla.EolMode.EolMac:
595 return "\r"
596 else:
597 return "\r\n"
598
599
600 def extractFlags(text):
601 """
602 Function to extract eric specific flags out of the given text.
603
604 Flags are contained in comments and are introduced by 'eflag:'.
605 The rest of the line is interpreted as 'key = value'. value is
606 analyzed for being an integer or float value. If that fails, it
607 is assumed to be a string. If a key does not contain a '='
608 character, it is assumed to be a boolean flag. Flags are expected
609 at the very end of a file. The search is ended, if a line without
610 the 'eflag:' marker is found.
611
612 @param text text to be scanned (string)
613 @return dictionary of string, boolean, complex, float and int
614 """
615 flags = {}
616 lines = text.rstrip().splitlines() if isinstance(text, str) else text
617 for line in reversed(lines):
618 try:
619 index = line.index("eflag:")
620 except ValueError:
621 # no flag found, don't look any further
622 break
623
624 flag = line[index + 6:].strip()
625 if "=" in flag:
626 key, value = flag.split("=", 1)
627 key = key.strip()
628 value = value.strip()
629
630 if value.lower() in ["true", "false", "yes", "no", "ok"]:
631 # it is a flag
632 flags[key] = value.lower() in ["true", "yes", "ok"]
633 continue
634
635 try:
636 # interpret as int first
637 value = int(value)
638 except ValueError:
639 with contextlib.suppress(ValueError):
640 # interpret as float next
641 value = float(value)
642
643 flags[key] = value
644 else:
645 # treat it as a boolean
646 if flag[0] == "-":
647 # false flags start with '-'
648 flags[flag[1:]] = False
649 else:
650 flags[flag] = True
651
652 return flags
653
654
655 def extractFlagsFromFile(filename):
656 """
657 Function to extract eric specific flags out of the given file.
658
659 @param filename name of the file to be scanned (string)
660 @return dictionary of string, boolean, complex, float and int
661 """
662 try:
663 source, encoding = readEncodedFile(filename)
664 except (UnicodeError, OSError):
665 return {}
666
667 return extractFlags(source)
668
669
670 def extractLineFlags(line, startComment="#", endComment="", flagsLine=False):
671 """
672 Function to extract flags starting and ending with '__' from a line
673 comment.
674
675 @param line line to extract flags from (string)
676 @param startComment string identifying the start of the comment (string)
677 @param endComment string identifying the end of a comment (string)
678 @param flagsLine flag indicating to check for a flags only line (bool)
679 @return list containing the extracted flags (list of strings)
680 """
681 flags = []
682
683 if not flagsLine or (
684 flagsLine and line.strip().startswith(startComment)):
685 pos = line.rfind(startComment)
686 if pos >= 0:
687 comment = line[pos + len(startComment):].strip()
688 if endComment:
689 endPos = line.rfind(endComment)
690 if endPos >= 0:
691 comment = comment[:endPos]
692 flags = [f.strip() for f in comment.split()
693 if (f.startswith("__") and f.endswith("__"))]
694 return flags
695
696
697 def filterAnsiSequences(txt):
698 """
699 Function to filter out ANSI escape sequences (color only).
700
701 @param txt text to be filtered
702 @type str
703 @return text without ANSI escape sequences
704 @rtype str
705 """
706 ntxt = txt[:]
707 while True:
708 start = ntxt.find("\33[") # find escape character
709 if start == -1:
710 break
711 end = ntxt.find("m", start)
712 if end == -1:
713 break
714 ntxt = ntxt[:start] + ntxt[end + 1:]
715
716 return ntxt
717
718
719 def toNativeSeparators(path):
720 """
721 Function returning a path, that is using native separator characters.
722
723 @param path path to be converted (string)
724 @return path with converted separator characters (string)
725 """
726 return QDir.toNativeSeparators(path)
727
728
729 def fromNativeSeparators(path):
730 """
731 Function returning a path, that is using "/" separator characters.
732
733 @param path path to be converted (string)
734 @return path with converted separator characters (string)
735 """
736 return QDir.fromNativeSeparators(path)
737
738
739 def normcasepath(path):
740 """
741 Function returning a path, that is normalized with respect to its case
742 and references.
743
744 @param path file path (string)
745 @return case normalized path (string)
746 """
747 return os.path.normcase(os.path.normpath(path))
748
749
750 def normcaseabspath(path):
751 """
752 Function returning an absolute path, that is normalized with respect to
753 its case and references.
754
755 @param path file path (string)
756 @return absolute, normalized path (string)
757 """
758 return os.path.normcase(os.path.abspath(path))
759
760
761 def normjoinpath(a, *p):
762 """
763 Function returning a normalized path of the joined parts passed into it.
764
765 @param a first path to be joined (string)
766 @param p variable number of path parts to be joined (string)
767 @return normalized path (string)
768 """
769 return os.path.normpath(os.path.join(a, *p))
770
771
772 def normabsjoinpath(a, *p):
773 """
774 Function returning a normalized, absolute path of the joined parts passed
775 into it.
776
777 @param a first path to be joined (string)
778 @param p variable number of path parts to be joind (string)
779 @return absolute, normalized path (string)
780 """
781 return os.path.abspath(os.path.join(a, *p))
782
783
784 def isinpath(file):
785 """
786 Function to check for an executable file.
787
788 @param file filename of the executable to check (string)
789 @return flag to indicate, if the executable file is accessible
790 via the searchpath defined by the PATH environment variable.
791 """
792 if os.path.isabs(file):
793 return os.access(file, os.X_OK)
794
795 if os.path.exists(os.path.join(os.curdir, file)):
796 return os.access(os.path.join(os.curdir, file), os.X_OK)
797
798 path = getEnvironmentEntry('PATH')
799
800 # environment variable not defined
801 if path is None:
802 return False
803
804 dirs = path.split(os.pathsep)
805 return any(os.access(os.path.join(directory, file), os.X_OK)
806 for directory in dirs)
807
808
809 def startswithPath(path, start):
810 """
811 Function to check, if a path starts with a given start path.
812
813 @param path path to be checked
814 @type str
815 @param start start path
816 @type str
817 @return flag indicating that the path starts with the given start
818 path
819 @rtype bool
820 """
821 return (
822 bool(start) and
823 (
824 path == start or
825 normcasepath(path).startswith(normcasepath(start + "/"))
826 )
827 )
828
829
830 def relativeUniversalPath(path, start):
831 """
832 Function to convert a file path to a path relative to a start path
833 with universal separators.
834
835 @param path file or directory name to convert (string)
836 @param start start path (string)
837 @return relative path or unchanged path, if path does not start with
838 the start path with universal separators (string)
839 """
840 return fromNativeSeparators(os.path.relpath(path, start))
841
842
843 def absolutePath(path, start):
844 """
845 Public method to convert a path relative to a start path to an
846 absolute path.
847
848 @param path file or directory name to convert (string)
849 @param start start path (string)
850 @return absolute path (string)
851 """
852 if not os.path.isabs(path):
853 path = os.path.normpath(os.path.join(start, path))
854 return path
855
856
857 def absoluteUniversalPath(path, start):
858 """
859 Public method to convert a path relative to a start path with
860 universal separators to an absolute path.
861
862 @param path file or directory name to convert (string)
863 @param start start path (string)
864 @return absolute path with native separators (string)
865 """
866 if not os.path.isabs(path):
867 path = toNativeSeparators(os.path.normpath(os.path.join(start, path)))
868 return path
869
870
871 def getExecutablePath(file):
872 """
873 Function to build the full path of an executable file from the environment.
874
875 @param file filename of the executable to check (string)
876 @return full executable name, if the executable file is accessible
877 via the searchpath defined by the PATH environment variable, or an
878 empty string otherwise.
879 """
880 if os.path.isabs(file):
881 if os.access(file, os.X_OK):
882 return file
883 else:
884 return ""
885
886 cur_path = os.path.join(os.curdir, file)
887 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
888 return cur_path
889
890 path = os.getenv('PATH')
891
892 # environment variable not defined
893 if path is None:
894 return ""
895
896 dirs = path.split(os.pathsep)
897 for directory in dirs:
898 exe = os.path.join(directory, file)
899 if os.access(exe, os.X_OK):
900 return exe
901
902 return ""
903
904
905 def getExecutablePaths(file):
906 """
907 Function to build all full path of an executable file from the environment.
908
909 @param file filename of the executable (string)
910 @return list of full executable names (list of strings), if the executable
911 file is accessible via the searchpath defined by the PATH environment
912 variable, or an empty list otherwise.
913 """
914 paths = []
915
916 if os.path.isabs(file):
917 if os.access(file, os.X_OK):
918 return [file]
919 else:
920 return []
921
922 cur_path = os.path.join(os.curdir, file)
923 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
924 paths.append(cur_path)
925
926 path = os.getenv('PATH')
927
928 # environment variable not defined
929 if path is not None:
930 dirs = path.split(os.pathsep)
931 for directory in dirs:
932 exe = os.path.join(directory, file)
933 if os.access(exe, os.X_OK) and exe not in paths:
934 paths.append(exe)
935
936 return paths
937
938
939 def getWindowsExecutablePath(file):
940 """
941 Function to build the full path of an executable file from the environment
942 on Windows platforms.
943
944 First an executable with the extension .exe is searched for, thereafter
945 such with the extensions .cmd or .bat and finally the given file name as
946 is. The first match is returned.
947
948 @param file filename of the executable to check (string)
949 @return full executable name, if the executable file is accessible
950 via the searchpath defined by the PATH environment variable, or an
951 empty string otherwise.
952 """
953 if os.path.isabs(file):
954 if os.access(file, os.X_OK):
955 return file
956 else:
957 return ""
958
959 filenames = [file + ".exe", file + ".cmd", file + ".bat", file]
960
961 for filename in filenames:
962 cur_path = os.path.join(os.curdir, filename)
963 if os.path.exists(cur_path) and os.access(cur_path, os.X_OK):
964 return os.path.abspath(cur_path)
965
966 path = os.getenv('PATH')
967
968 # environment variable not defined
969 if path is None:
970 return ""
971
972 dirs = path.split(os.pathsep)
973 for directory in dirs:
974 for filename in filenames:
975 exe = os.path.join(directory, filename)
976 if os.access(exe, os.X_OK):
977 return exe
978
979 return ""
980
981
982 def isExecutable(exe):
983 """
984 Function to check, if a file is executable.
985
986 @param exe filename of the executable to check (string)
987 @return flag indicating executable status (boolean)
988 """
989 return os.access(exe, os.X_OK)
990
991
992 def isDrive(path):
993 """
994 Function to check, if a path is a Windows drive.
995
996 @param path path name to be checked
997 @type str
998 @return flag indicating a Windows drive
999 @rtype bool
1000 """
1001 isDrive = False
1002 drive, directory = os.path.splitdrive(path)
1003 if (
1004 drive and
1005 len(drive) == 2 and
1006 drive.endswith(":") and
1007 directory in ["", "\\", "/"]
1008 ):
1009 isDrive = True
1010
1011 return isDrive
1012
1013
1014 def samepath(f1, f2):
1015 """
1016 Function to compare two paths.
1017
1018 @param f1 first path for the compare (string)
1019 @param f2 second path for the compare (string)
1020 @return flag indicating whether the two paths represent the
1021 same path on disk.
1022 """
1023 if f1 is None or f2 is None:
1024 return False
1025
1026 if (
1027 normcaseabspath(os.path.realpath(f1)) ==
1028 normcaseabspath(os.path.realpath(f2))
1029 ):
1030 return True
1031
1032 return False
1033
1034
1035 def samefilepath(f1, f2):
1036 """
1037 Function to compare two paths. Strips the filename.
1038
1039 @param f1 first filepath for the compare (string)
1040 @param f2 second filepath for the compare (string)
1041 @return flag indicating whether the two paths represent the
1042 same path on disk.
1043 """
1044 if f1 is None or f2 is None:
1045 return False
1046
1047 if (normcaseabspath(os.path.dirname(os.path.realpath(f1))) ==
1048 normcaseabspath(os.path.dirname(os.path.realpath(f2)))):
1049 return True
1050
1051 return False
1052
1053 try:
1054 EXTSEP = os.extsep
1055 except AttributeError:
1056 EXTSEP = "."
1057
1058
1059 def splitPath(name):
1060 """
1061 Function to split a pathname into a directory part and a file part.
1062
1063 @param name path name (string)
1064 @return a tuple of 2 strings (dirname, filename).
1065 """
1066 if os.path.isdir(name):
1067 dn = os.path.abspath(name)
1068 fn = "."
1069 else:
1070 dn, fn = os.path.split(name)
1071 return (dn, fn)
1072
1073
1074 def joinext(prefix, ext):
1075 """
1076 Function to join a file extension to a path.
1077
1078 The leading "." of ext is replaced by a platform specific extension
1079 separator if necessary.
1080
1081 @param prefix the basepart of the filename (string)
1082 @param ext the extension part (string)
1083 @return the complete filename (string)
1084 """
1085 if ext[0] != ".":
1086 ext = ".{0}".format(ext)
1087 # require leading separator to match os.path.splitext
1088 return prefix + EXTSEP + ext[1:]
1089
1090
1091 def compactPath(path, width, measure=len):
1092 """
1093 Function to return a compacted path fitting inside the given width.
1094
1095 @param path path to be compacted (string)
1096 @param width width for the compacted path (integer)
1097 @param measure reference to a function used to measure the length of the
1098 string
1099 @return compacted path (string)
1100 """
1101 if measure(path) <= width:
1102 return path
1103
1104 ellipsis = '...'
1105
1106 head, tail = os.path.split(path)
1107 mid = len(head) // 2
1108 head1 = head[:mid]
1109 head2 = head[mid:]
1110 while head1:
1111 # head1 is same size as head2 or one shorter
1112 path = os.path.join("{0}{1}{2}".format(head1, ellipsis, head2), tail)
1113 if measure(path) <= width:
1114 return path
1115 head1 = head1[:-1]
1116 head2 = head2[1:]
1117 path = os.path.join(ellipsis, tail)
1118 if measure(path) <= width:
1119 return path
1120 while tail:
1121 path = "{0}{1}".format(ellipsis, tail)
1122 if measure(path) <= width:
1123 return path
1124 tail = tail[1:]
1125 return ""
1126
1127
1128 def direntries(path, filesonly=False, pattern=None, followsymlinks=True,
1129 checkStop=None):
1130 """
1131 Function returning a list of all files and directories.
1132
1133 @param path root of the tree to check
1134 @type str
1135 @param filesonly flag indicating that only files are wanted
1136 @type bool
1137 @param pattern a filename pattern or list of filename patterns to check
1138 against
1139 @type str or list of str
1140 @param followsymlinks flag indicating whether symbolic links
1141 should be followed
1142 @type bool
1143 @param checkStop function to be called to check for a stop
1144 @type function
1145 @return list of all files and directories in the tree rooted
1146 at path. The names are expanded to start with path.
1147 @rtype list of strs
1148 """
1149 patterns = pattern if isinstance(pattern, list) else [pattern]
1150 files = [] if filesonly else [path]
1151 try:
1152 entries = os.listdir(path)
1153 for entry in entries:
1154 if checkStop and checkStop():
1155 break
1156
1157 if entry in ['.svn',
1158 '.hg',
1159 '.git',
1160 '.ropeproject',
1161 '.eric7project',
1162 '.jedi']:
1163 continue
1164
1165 fentry = os.path.join(path, entry)
1166 if (
1167 pattern and
1168 not os.path.isdir(fentry) and
1169 not any(fnmatch.fnmatch(entry, p) for p in patterns)
1170 ):
1171 # entry doesn't fit the given pattern
1172 continue
1173
1174 if os.path.isdir(fentry):
1175 if os.path.islink(fentry) and not followsymlinks:
1176 continue
1177 files += direntries(
1178 fentry, filesonly, pattern, followsymlinks, checkStop)
1179 else:
1180 files.append(fentry)
1181 except OSError:
1182 pass
1183 except UnicodeDecodeError:
1184 pass
1185 return files
1186
1187
1188 def getDirs(path, excludeDirs):
1189 """
1190 Function returning a list of all directories below path.
1191
1192 @param path root of the tree to check
1193 @param excludeDirs basename of directories to ignore
1194 @return list of all directories found
1195 """
1196 try:
1197 names = os.listdir(path)
1198 except OSError:
1199 return []
1200
1201 dirs = []
1202 for name in names:
1203 if (
1204 os.path.isdir(os.path.join(path, name)) and
1205 not os.path.islink(os.path.join(path, name))
1206 ):
1207 exclude = 0
1208 for e in excludeDirs:
1209 if name.split(os.sep, 1)[0] == e:
1210 exclude = 1
1211 break
1212 if not exclude:
1213 dirs.append(os.path.join(path, name))
1214
1215 for name in dirs[:]:
1216 if not os.path.islink(name):
1217 dirs += getDirs(name, excludeDirs)
1218
1219 return dirs
1220
1221
1222 def findVolume(volumeName, findAll=False):
1223 """
1224 Function to find the directory belonging to a given volume name.
1225
1226 @param volumeName name of the volume to search for
1227 @type str
1228 @param findAll flag indicating to get the directories for all volumes
1229 starting with the given name (defaults to False)
1230 @type bool (optional)
1231 @return directory path or list of directory paths for the given volume
1232 name
1233 @rtype str or list of str
1234 """
1235 volumeDirectories = []
1236 volumeDirectory = None
1237
1238 if isWindowsPlatform():
1239 # we are on a Windows platform
1240 def getVolumeName(diskName):
1241 """
1242 Local function to determine the volume of a disk or device.
1243
1244 Each disk or external device connected to windows has an
1245 attribute called "volume name". This function returns the
1246 volume name for the given disk/device.
1247
1248 Code from http://stackoverflow.com/a/12056414
1249 """
1250 volumeNameBuffer = ctypes.create_unicode_buffer(1024)
1251 ctypes.windll.kernel32.GetVolumeInformationW(
1252 ctypes.c_wchar_p(diskName), volumeNameBuffer,
1253 ctypes.sizeof(volumeNameBuffer), None, None, None, None, 0)
1254 return volumeNameBuffer.value
1255
1256 #
1257 # In certain circumstances, volumes are allocated to USB
1258 # storage devices which cause a Windows popup to raise if their
1259 # volume contains no media. Wrapping the check in SetErrorMode
1260 # with SEM_FAILCRITICALERRORS (1) prevents this popup.
1261 #
1262 oldMode = ctypes.windll.kernel32.SetErrorMode(1)
1263 try:
1264 for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
1265 dirpath = "{0}:\\".format(disk)
1266 if os.path.exists(dirpath):
1267 if findAll:
1268 if getVolumeName(dirpath).startswith(volumeName):
1269 volumeDirectories.append(dirpath)
1270 else:
1271 if getVolumeName(dirpath) == volumeName:
1272 volumeDirectory = dirpath
1273 break
1274 finally:
1275 ctypes.windll.kernel32.SetErrorMode(oldMode)
1276 else:
1277 # we are on a Linux or macOS platform
1278 for mountCommand in ["mount", "/sbin/mount", "/usr/sbin/mount"]:
1279 with contextlib.suppress(FileNotFoundError):
1280 mountOutput = subprocess.run( # secok
1281 mountCommand, check=True, capture_output=True, text=True
1282 ).stdout.splitlines()
1283 mountedVolumes = [
1284 x.split(" type")[0].split(maxsplit=2)[2]
1285 for x in mountOutput
1286 ]
1287 if findAll:
1288 for volume in mountedVolumes:
1289 if volumeName in volume:
1290 volumeDirectories.append(volume)
1291 if volumeDirectories:
1292 break
1293 else:
1294 for volume in mountedVolumes:
1295 if volume.endswith(volumeName):
1296 volumeDirectory = volume
1297 break
1298 if volumeDirectory:
1299 break
1300
1301 if findAll:
1302 return volumeDirectories
1303 else:
1304 return volumeDirectory
1305
1306
1307 def getTestFileNames(fn):
1308 """
1309 Function to build the potential file names of a test file.
1310
1311 The file names for the test file is built by prepending the string
1312 "test" and "test_" to the file name passed into this function and
1313 by appending the string "_test".
1314
1315 @param fn file name basis to be used for the test file names
1316 @type str
1317 @return file names of the corresponding test file
1318 @rtype list of str
1319 """
1320 dn, fn = os.path.split(fn)
1321 fn, ext = os.path.splitext(fn)
1322 prefixes = ["test", "test_"]
1323 postfixes = ["_test"]
1324 return [
1325 os.path.join(dn, "{0}{1}{2}".format(prefix, fn, ext))
1326 for prefix in prefixes
1327 ] + [
1328 os.path.join(dn, "{0}{1}{2}".format(fn, postfix, ext))
1329 for postfix in postfixes
1330 ]
1331
1332
1333 def getCoverageFileNames(fn):
1334 """
1335 Function to build a list of coverage data file names.
1336
1337 @param fn file name basis to be used for the coverage data file
1338 @type str
1339 @return list of existing coverage data files
1340 @rtype list of str
1341 """
1342 files = []
1343 for filename in [fn, os.path.dirname(fn) + os.sep] + getTestFileNames(fn):
1344 f = getCoverageFileName(filename)
1345 if f:
1346 files.append(f)
1347 return files
1348
1349
1350 def getCoverageFileName(fn, mustExist=True):
1351 """
1352 Function to build a file name for a coverage data file.
1353
1354 @param fn file name basis to be used for the coverage data file name
1355 @type str
1356 @param mustExist flag indicating to check that the file exists (defaults
1357 to True)
1358 @type bool (optional)
1359 @return coverage data file name
1360 @rtype str
1361 """
1362 basename = os.path.splitext(fn)[0]
1363 filename = "{0}.coverage".format(basename)
1364 if mustExist:
1365 if os.path.isfile(filename):
1366 return filename
1367 else:
1368 return ""
1369 else:
1370 return filename
1371
1372
1373 def getProfileFileNames(fn):
1374 """
1375 Function to build a list of profile data file names.
1376
1377 @param fn file name basis to be used for the profile data file
1378 @type str
1379 @return list of existing profile data files
1380 @rtype list of str
1381 """
1382 files = []
1383 for filename in [fn, os.path.dirname(fn) + os.sep] + getTestFileNames(fn):
1384 f = getProfileFileName(filename)
1385 if f:
1386 files.append(f)
1387 return files
1388
1389
1390 def getProfileFileName(fn, mustExist=True):
1391 """
1392 Function to build a file name for a profile data file.
1393
1394 @param fn file name basis to be used for the profile data file name
1395 @type str
1396 @param mustExist flag indicating to check that the file exists (defaults
1397 to True)
1398 @type bool (optional)
1399 @return profile data file name
1400 @rtype str
1401 """
1402 basename = os.path.splitext(fn)[0]
1403 filename = "{0}.profile".format(basename)
1404 if mustExist:
1405 if os.path.isfile(filename):
1406 return filename
1407 else:
1408 return ""
1409 else:
1410 return filename
1411
1412
1413 def parseOptionString(s):
1414 """
1415 Function used to convert an option string into a list of options.
1416
1417 @param s option string
1418 @type str
1419 @return list of options
1420 @rtype list of str
1421 """
1422 s = re.sub(r"%[A-Z%]", _percentReplacementFunc, s)
1423 return shlex.split(s)
1424
1425
1426 def _percentReplacementFunc(matchobj):
1427 """
1428 Protected function called for replacing % codes.
1429
1430 @param matchobj match object for the code
1431 @type re.Match
1432 @return replacement string
1433 @rtype str
1434 """
1435 return getPercentReplacement(matchobj.group(0))
1436
1437
1438 def getPercentReplacement(code):
1439 """
1440 Function to get the replacement for code.
1441
1442 @param code code indicator
1443 @type str
1444 @return replacement string
1445 @rtype str
1446 """
1447 if code in ["C", "%C"]:
1448 # column of the cursor of the current editor
1449 aw = ericApp().getObject("ViewManager").activeWindow()
1450 if aw is None:
1451 column = -1
1452 else:
1453 column = aw.getCursorPosition()[1]
1454 return "{0:d}".format(column)
1455 elif code in ["D", "%D"]:
1456 # directory of active editor
1457 aw = ericApp().getObject("ViewManager").activeWindow()
1458 if aw is None:
1459 dn = "not_available"
1460 else:
1461 fn = aw.getFileName()
1462 if fn is None:
1463 dn = "not_available"
1464 else:
1465 dn = os.path.dirname(fn)
1466 return dn
1467 elif code in ["F", "%F"]:
1468 # filename (complete) of active editor
1469 aw = ericApp().getObject("ViewManager").activeWindow()
1470 if aw is None:
1471 fn = "not_available"
1472 else:
1473 fn = aw.getFileName()
1474 if fn is None:
1475 fn = "not_available"
1476 return fn
1477 elif code in ["H", "%H"]:
1478 # home directory
1479 return getHomeDir()
1480 elif code in ["L", "%L"]:
1481 # line of the cursor of the current editor
1482 aw = ericApp().getObject("ViewManager").activeWindow()
1483 if aw is None:
1484 line = 0
1485 else:
1486 line = aw.getCursorPosition()[0] + 1
1487 return "{0:d}".format(line)
1488 elif code in ["P", "%P"]:
1489 # project path
1490 projectPath = ericApp().getObject("Project").getProjectPath()
1491 if not projectPath:
1492 projectPath = "not_available"
1493 return projectPath
1494 elif code in ["S", "%S"]:
1495 # selected text of the current editor
1496 aw = ericApp().getObject("ViewManager").activeWindow()
1497 if aw is None:
1498 text = "not_available"
1499 else:
1500 text = aw.selectedText()
1501 return text
1502 elif code in ["U", "%U"]:
1503 # username
1504 un = getUserName()
1505 if un is None:
1506 return code
1507 else:
1508 return un
1509 elif code in ["%", "%%"]:
1510 # the percent sign
1511 return "%"
1512 else:
1513 # unknown code, just return it
1514 return code
1515
1516
1517 def getPercentReplacementHelp():
1518 """
1519 Function to get the help text for the supported %-codes.
1520
1521 @returns help text (string)
1522 """
1523 return QCoreApplication.translate(
1524 "Utilities",
1525 """<p>You may use %-codes as placeholders in the string."""
1526 """ Supported codes are:"""
1527 """<table>"""
1528 """<tr><td>%C</td><td>column of the cursor of the current editor"""
1529 """</td></tr>"""
1530 """<tr><td>%D</td><td>directory of the current editor</td></tr>"""
1531 """<tr><td>%F</td><td>filename of the current editor</td></tr>"""
1532 """<tr><td>%H</td><td>home directory of the current user</td></tr>"""
1533 """<tr><td>%L</td><td>line of the cursor of the current editor"""
1534 """</td></tr>"""
1535 """<tr><td>%P</td><td>path of the current project</td></tr>"""
1536 """<tr><td>%S</td><td>selected text of the current editor</td></tr>"""
1537 """<tr><td>%U</td><td>username of the current user</td></tr>"""
1538 """<tr><td>%%</td><td>the percent sign</td></tr>"""
1539 """</table>"""
1540 """</p>""")
1541
1542
1543 def getUserName():
1544 """
1545 Function to get the user name.
1546
1547 @return user name (string)
1548 """
1549 user = getpass.getuser()
1550
1551 if isWindowsPlatform() and not user:
1552 return win32_GetUserName()
1553
1554 return user
1555
1556
1557 def getRealName():
1558 """
1559 Function to get the real name of the user.
1560
1561 @return real name of the user (string)
1562 """
1563 if isWindowsPlatform():
1564 return win32_getRealName()
1565 else:
1566 import pwd
1567 user = getpass.getuser()
1568 return pwd.getpwnam(user).pw_gecos
1569
1570
1571 def getHomeDir():
1572 """
1573 Function to get a users home directory.
1574
1575 @return home directory (string)
1576 """
1577 return QDir.homePath()
1578
1579
1580 def getPythonLibPath():
1581 """
1582 Function to determine the path to Python's library.
1583
1584 @return path to the Python library (string)
1585 """
1586 pyFullVers = sys.version.split()[0]
1587
1588 vl = re.findall("[0-9.]*", pyFullVers)[0].split(".")
1589 major = vl[0]
1590 minor = vl[1]
1591
1592 pyVers = major + "." + minor
1593
1594 if isWindowsPlatform():
1595 libDir = sys.prefix + "\\Lib"
1596 else:
1597 try:
1598 syslib = sys.lib
1599 except AttributeError:
1600 syslib = "lib"
1601 libDir = sys.prefix + "/" + syslib + "/python" + pyVers
1602
1603 return libDir
1604
1605
1606 def getPythonVersion():
1607 """
1608 Function to get the Python version (major, minor) as an integer value.
1609
1610 @return An integer representing major and minor version number (integer)
1611 """
1612 return sys.hexversion >> 16
1613
1614
1615 def determinePythonVersion(filename, source, editor=None):
1616 """
1617 Function to determine the python version of a given file.
1618
1619 @param filename name of the file with extension (str)
1620 @param source of the file (str)
1621 @param editor reference to the editor, if the file is opened
1622 already (Editor object)
1623 @return Python version if file is Python3 (int)
1624 """
1625 pyAssignment = {
1626 "Python3": 3,
1627 "MicroPython": 3,
1628 "Cython": 3,
1629 }
1630
1631 if not editor:
1632 viewManager = ericApp().getObject('ViewManager')
1633 editor = viewManager.getOpenEditor(filename)
1634
1635 # Maybe the user has changed the language
1636 if editor and editor.getFileType() in pyAssignment:
1637 return pyAssignment[editor.getFileType()]
1638
1639 pyVer = 0
1640 if filename:
1641 if not source:
1642 source = readEncodedFile(filename)[0]
1643 flags = extractFlags(source)
1644 ext = os.path.splitext(filename)[1]
1645 py3Ext = Preferences.getPython("Python3Extensions")
1646 project = ericApp().getObject('Project')
1647 basename = os.path.basename(filename)
1648
1649 if "FileType" in flags:
1650 pyVer = pyAssignment.get(flags["FileType"], 0)
1651 elif project.isOpen() and project.isProjectFile(filename):
1652 language = project.getEditorLexerAssoc(basename)
1653 if not language:
1654 language = Preferences.getEditorLexerAssoc(basename)
1655 if language == 'Python3':
1656 pyVer = pyAssignment[language]
1657
1658 if pyVer:
1659 # Skip the next tests
1660 pass
1661 elif (Preferences.getProject("DeterminePyFromProject") and
1662 project.isOpen() and
1663 project.isProjectFile(filename) and
1664 ext in py3Ext):
1665 pyVer = pyAssignment.get(project.getProjectLanguage(), 0)
1666 elif ext in py3Ext:
1667 pyVer = 3
1668 elif source:
1669 if isinstance(source, str):
1670 line0 = source.splitlines()[0]
1671 else:
1672 line0 = source[0]
1673 if (
1674 line0.startswith("#!") and
1675 (("python3" in line0) or ("python" in line0))
1676 ):
1677 pyVer = 3
1678
1679 if pyVer == 0 and ext in py3Ext:
1680 pyVer = 3
1681
1682 return pyVer
1683
1684
1685 def rxIndex(rx, txt):
1686 """
1687 Function to get the index (start position) of a regular expression match
1688 within some text.
1689
1690 @param rx regular expression object as created by re.compile()
1691 @type re.Pattern
1692 @param txt text to be scanned
1693 @type str
1694 @return start position of the match or -1 indicating no match was found
1695 @rtype int
1696 """
1697 match = rx.search(txt)
1698 if match is None:
1699 return -1
1700 else:
1701 return match.start()
1702
1703
1704 ###############################################################################
1705 ## functions for environment handling
1706 ###############################################################################
1707
1708
1709 def getEnvironmentEntry(key, default=None):
1710 """
1711 Module function to get an environment entry.
1712
1713 @param key key of the requested environment entry (string)
1714 @param default value to be returned, if the environment doesn't contain
1715 the requested entry (string)
1716 @return the requested entry or the default value, if the entry wasn't
1717 found (string or None)
1718 """
1719 pattern = "^{0}[ \t]*=".format(key)
1720 filterRe = (
1721 re.compile(pattern, re.IGNORECASE)
1722 if isWindowsPlatform() else
1723 re.compile(pattern)
1724 )
1725
1726 entries = [e for e in QProcess.systemEnvironment()
1727 if filterRe.search(e) is not None]
1728 if not entries:
1729 return default
1730
1731 # if there are multiple entries, just consider the first one
1732 ename, value = entries[0].split("=", 1)
1733 return value.strip()
1734
1735
1736 def hasEnvironmentEntry(key):
1737 """
1738 Module function to check, if the environment contains an entry.
1739
1740 @param key key of the requested environment entry
1741 @type str
1742 @return flag indicating the presence of the requested entry
1743 @rtype bool
1744 """
1745 pattern = "^{0}[ \t]*=".format(key)
1746 filterRe = (
1747 re.compile(pattern, re.IGNORECASE)
1748 if isWindowsPlatform() else
1749 re.compile(pattern)
1750 )
1751
1752 entries = [e for e in QProcess.systemEnvironment()
1753 if filterRe.search(e) is not None]
1754 return len(entries) > 0
1755
1756 ###############################################################################
1757 ## Qt utility functions below
1758 ###############################################################################
1759
1760
1761 def generateQtToolName(toolname):
1762 """
1763 Module function to generate the executable name for a Qt tool like
1764 designer.
1765
1766 @param toolname base name of the tool (string)
1767 @return the Qt tool name without extension (string)
1768 """
1769 return "{0}{1}{2}".format(Preferences.getQt("QtToolsPrefix"),
1770 toolname,
1771 Preferences.getQt("QtToolsPostfix")
1772 )
1773
1774
1775 def getQtMacBundle(toolname):
1776 """
1777 Module function to determine the correct Mac OS X bundle name for Qt tools.
1778
1779 @param toolname plain name of the tool (e.g. "designer") (string)
1780 @return bundle name of the Qt tool (string)
1781 """
1782 qtDir = getQtBinariesPath()
1783 bundles = [
1784 os.path.join(
1785 qtDir, 'bin', generateQtToolName(toolname.capitalize())) + ".app",
1786 os.path.join(qtDir, 'bin', generateQtToolName(toolname)) + ".app",
1787 os.path.join(
1788 qtDir, generateQtToolName(toolname.capitalize())) + ".app",
1789 os.path.join(qtDir, generateQtToolName(toolname)) + ".app",
1790 ]
1791 if toolname == "designer":
1792 # support the standalone Qt Designer installer from
1793 # https://build-system.fman.io/qt-designer-download
1794 designer = "Qt Designer.app"
1795 bundles.extend([
1796 os.path.join(qtDir, 'bin', designer),
1797 os.path.join(qtDir, designer),
1798 ])
1799 for bundle in bundles:
1800 if os.path.exists(bundle):
1801 return bundle
1802 return ""
1803
1804
1805 def prepareQtMacBundle(toolname, args):
1806 """
1807 Module function for starting Qt tools that are Mac OS X bundles.
1808
1809 @param toolname plain name of the tool (e.g. "designer")
1810 @type str
1811 @param args name of input file for tool, if any
1812 @type list of str
1813 @return command-name and args for QProcess
1814 @rtype tuple of (str, list of str)
1815 """
1816 fullBundle = getQtMacBundle(toolname)
1817 if fullBundle == "":
1818 return ("", [])
1819
1820 newArgs = []
1821 newArgs.append("-a")
1822 newArgs.append(fullBundle)
1823 if args:
1824 newArgs.append("--args")
1825 newArgs += args
1826
1827 return ("open", newArgs)
1828
1829 ###############################################################################
1830 ## PyQt utility functions below
1831 ###############################################################################
1832
1833
1834 def generatePyQtToolPath(toolname, alternatives=None):
1835 """
1836 Module function to generate the executable path for a PyQt tool.
1837
1838 @param toolname base name of the tool
1839 @type str
1840 @param alternatives list of alternative tool names to try
1841 @type list of str
1842 @return executable path name of the tool
1843 @rtype str
1844 """
1845 pyqtVariant = int(toolname[-1])
1846 pyqtToolsPath = getPyQtToolsPath(pyqtVariant)
1847 if pyqtToolsPath:
1848 exe = os.path.join(pyqtToolsPath, toolname)
1849 if isWindowsPlatform():
1850 exe += ".exe"
1851 else:
1852 if isWindowsPlatform():
1853 exe = getWindowsExecutablePath(toolname)
1854 else:
1855 exe = toolname
1856
1857 if not isinpath(exe) and alternatives:
1858 ex_ = generatePyQtToolPath(alternatives[0], alternatives[1:])
1859 if isinpath(ex_):
1860 exe = ex_
1861
1862 return exe
1863
1864 ###############################################################################
1865 ## PySide2/PySide6 utility functions below
1866 ###############################################################################
1867
1868
1869 def generatePySideToolPath(toolname, variant=2):
1870 """
1871 Module function to generate the executable path for a PySide2/PySide6 tool.
1872
1873 @param toolname base name of the tool
1874 @type str
1875 @param variant indicator for the PySide variant
1876 @type int or str
1877 @return the PySide2/PySide6 tool path with extension
1878 @rtype str
1879 """
1880 if isWindowsPlatform():
1881 hasPyside = checkPyside(variant)
1882 if not hasPyside:
1883 return ""
1884
1885 venvName = Preferences.getQt("PySide{0}VenvName".format(variant))
1886 if not venvName:
1887 venvName = Preferences.getDebugger("Python3VirtualEnv")
1888 interpreter = ericApp().getObject(
1889 "VirtualEnvManager").getVirtualenvInterpreter(venvName)
1890 if interpreter == "" or not isinpath(interpreter):
1891 interpreter = getPythonExecutable()
1892 prefix = os.path.dirname(interpreter)
1893 if not prefix.endswith("Scripts"):
1894 prefix = os.path.join(prefix, "Scripts")
1895 return os.path.join(prefix, toolname + '.exe')
1896 else:
1897 # step 1: check, if the user has configured a tools path
1898 path = Preferences.getQt("PySide{0}ToolsDir".format(variant))
1899 if path:
1900 return os.path.join(path, toolname)
1901
1902 # step 2: determine from used Python interpreter
1903 dirName = os.path.dirname(sys.executable)
1904 if os.path.exists(os.path.join(dirName, toolname)):
1905 return os.path.join(dirName, toolname)
1906
1907 return toolname
1908
1909
1910 @functools.lru_cache()
1911 def checkPyside(variant=2):
1912 """
1913 Module function to check the presence of PySide2/PySide6.
1914
1915 @param variant indicator for the PySide variant
1916 @type int or str
1917 @return flags indicating the presence of PySide2/PySide6
1918 @rtype bool
1919 """
1920 venvName = Preferences.getQt("PySide{0}VenvName".format(variant))
1921 if not venvName:
1922 venvName = Preferences.getDebugger("Python3VirtualEnv")
1923 interpreter = ericApp().getObject(
1924 "VirtualEnvManager").getVirtualenvInterpreter(venvName)
1925 if interpreter == "" or not isinpath(interpreter):
1926 interpreter = getPythonExecutable()
1927
1928 checker = os.path.join(
1929 getConfig('ericDir'), "Utilities", "PySideImporter.py")
1930 args = [checker, "--variant={0}".format(variant)]
1931 proc = QProcess()
1932 proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
1933 proc.start(interpreter, args)
1934 finished = proc.waitForFinished(30000)
1935 return finished and proc.exitCode() == 0
1936
1937 ###############################################################################
1938 ## Other utility functions below
1939 ###############################################################################
1940
1941
1942 def generateVersionInfo(linesep='\n'):
1943 """
1944 Module function to generate a string with various version infos.
1945
1946 @param linesep string to be used to separate lines
1947 @type str
1948 @return string with version infos
1949 @rtype str
1950 """
1951 try:
1952 try:
1953 from PyQt6 import sip
1954 except ImportError:
1955 import sip
1956 sip_version_str = sip.SIP_VERSION_STR
1957 except (ImportError, AttributeError):
1958 sip_version_str = "sip version not available"
1959
1960 sizeStr = "64-Bit" if sys.maxsize > 2**32 else "32-Bit"
1961
1962 info = ["Version Numbers:"]
1963
1964 info.append(" Python {0}, {1}".format(sys.version.split()[0], sizeStr))
1965 info.append(" Qt {0}".format(qVersion()))
1966 info.append(" PyQt6 {0}".format(PYQT_VERSION_STR))
1967 try:
1968 from PyQt6 import QtCharts
1969 info.append(" PyQt6-Charts {0}".format(
1970 QtCharts.PYQT_CHART_VERSION_STR))
1971 except (ImportError, AttributeError):
1972 info.append(" PyQt6-Charts not installed")
1973 try:
1974 from PyQt6 import QtWebEngineCore
1975 info.append(" PyQt6-WebEngine {0}".format(
1976 QtWebEngineCore.PYQT_WEBENGINE_VERSION_STR))
1977 except (ImportError, AttributeError):
1978 info.append(" PyQt6-WebEngine not installed")
1979 info.append(" PyQt6-QScintilla {0}".format(QSCINTILLA_VERSION_STR))
1980 info.append(" sip {0}".format(sip_version_str))
1981 with contextlib.suppress(ImportError):
1982 from PyQt6 import QtWebEngineWidgets # __IGNORE_WARNING__
1983 from WebBrowser.Tools import WebBrowserTools
1984 chromiumVersion, chromiumSecurityVersion = (
1985 WebBrowserTools.getWebEngineVersions()[0:2]
1986 )
1987 info.append(" WebEngine {0}".format(chromiumVersion))
1988 if chromiumSecurityVersion:
1989 info.append(" (Security) {0}".format(chromiumSecurityVersion))
1990 info.append(" {0} {1}".format(Program, Version))
1991 info.append("")
1992 info.append("Platform: {0}".format(sys.platform))
1993 info.append(sys.version)
1994 desktop = desktopName()
1995 if desktop:
1996 info.append("")
1997 info.append("Desktop: {0}".format(desktop))
1998 session = sessionType()
1999 if session:
2000 info.append("")
2001 info.append("Session Type: {0}".format(session))
2002
2003 return linesep.join(info)
2004
2005
2006 def generatePluginsVersionInfo(linesep='\n'):
2007 """
2008 Module function to generate a string with plugins version infos.
2009
2010 @param linesep string to be used to separate lines
2011 @type str
2012 @return string with plugins version infos
2013 @rtype str
2014 """
2015 info = []
2016 app = ericApp()
2017 if app is not None:
2018 with contextlib.suppress(KeyError):
2019 pm = app.getObject("PluginManager")
2020 versions = {}
2021 for pinfo in pm.getPluginInfos():
2022 versions[pinfo["module_name"]] = pinfo["version"]
2023
2024 info.append("Plugins Version Numbers:")
2025 for pluginModuleName in sorted(versions.keys()):
2026 info.append(" {0} {1}".format(
2027 pluginModuleName, versions[pluginModuleName]))
2028
2029 return linesep.join(info)
2030
2031
2032 def generateDistroInfo(linesep='\n'):
2033 """
2034 Module function to generate a string with distribution infos.
2035
2036 @param linesep string to be used to separate lines
2037 @type str
2038 @return string with distribution infos
2039 @rtype str
2040 """
2041 info = []
2042 if isLinuxPlatform():
2043 releaseList = glob.glob("/etc/*-release")
2044 if releaseList:
2045 info.append("Distribution Info:")
2046 for rfile in releaseList:
2047 try:
2048 with open(rfile, "r") as f:
2049 lines = f.read().splitlines()
2050 except OSError:
2051 continue
2052
2053 info.append(' {0}'.format(rfile))
2054 info.extend([' {0}'.format(line) for line in lines])
2055 info.append("")
2056
2057 return linesep.join(info)
2058
2059
2060 def toBool(dataStr):
2061 """
2062 Module function to convert a string to a boolean value.
2063
2064 @param dataStr string to be converted (string)
2065 @return converted boolean value (boolean)
2066 """
2067 if dataStr in ["True", "true", "1", "Yes", "yes"]:
2068 return True
2069 elif dataStr in ["False", "false", "0", "No", "no"]:
2070 return False
2071 else:
2072 return bool(dataStr)
2073
2074
2075 def getSysPath(interpreter):
2076 """
2077 Module function to get the Python path (sys.path) of a specific
2078 interpreter.
2079
2080 @param interpreter Python interpreter executable to get sys.path for
2081 @type str
2082 @return list containing sys.path of the interpreter; an empty list
2083 is returned, if the interpreter is the one used to run eric itself
2084 @rtype list of str
2085 """
2086 import json
2087
2088 sysPath = []
2089
2090 getSysPathSkript = os.path.join(
2091 os.path.dirname(__file__), "GetSysPath.py")
2092 args = [getSysPathSkript]
2093 proc = QProcess()
2094 proc.setProcessChannelMode(QProcess.ProcessChannelMode.MergedChannels)
2095 proc.start(interpreter, args)
2096 finished = proc.waitForFinished(30000)
2097 if finished and proc.exitCode() == 0:
2098 text = proc.readAllStandardOutput()
2099 sysPathResult = str(text, "utf-8", "replace").strip()
2100 with contextlib.suppress(TypeError, ValueError):
2101 sysPath = json.loads(sysPathResult)
2102 if "" in sysPath:
2103 sysPath.remove("")
2104
2105 return sysPath
2106
2107 ###############################################################################
2108 ## posix compatibility functions below
2109 ###############################################################################
2110
2111 # None right now
2112
2113 ###############################################################################
2114 ## win32 compatibility functions below
2115 ###############################################################################
2116
2117
2118 def win32_Kill(pid):
2119 """
2120 Function to provide an os.kill equivalent for Win32.
2121
2122 @param pid process id (integer)
2123 @return result of the kill (boolean)
2124 """
2125 import win32api
2126 handle = win32api.OpenProcess(1, 0, pid)
2127 return (0 != win32api.TerminateProcess(handle, 0))
2128
2129
2130 def win32_GetUserName():
2131 """
2132 Function to get the user name under Win32.
2133
2134 @return user name (string)
2135 """
2136 try:
2137 import win32api
2138 return win32api.GetUserName()
2139 except ImportError:
2140 try:
2141 u = getEnvironmentEntry('USERNAME')
2142 except KeyError:
2143 u = getEnvironmentEntry('username', None)
2144 return u
2145
2146
2147 def win32_getRealName():
2148 """
2149 Function to get the user's real name (aka. display name) under Win32.
2150
2151 @return real name of the current user (string)
2152 """
2153 import ctypes
2154
2155 GetUserNameEx = ctypes.windll.secur32.GetUserNameExW
2156 NameDisplay = 3
2157
2158 size = ctypes.pointer(ctypes.c_ulong(0))
2159 GetUserNameEx(NameDisplay, None, size)
2160
2161 nameBuffer = ctypes.create_unicode_buffer(size.contents.value)
2162 GetUserNameEx(NameDisplay, nameBuffer, size)
2163 return nameBuffer.value

eric ide

mercurial