diff --git a/BUILDING.md b/BUILDING.md index 89237b72..9591eff6 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -1,126 +1,152 @@ # Belgelerin İnşası -Buradaki belgeler [reStructuredText](http://docutils.sourceforge.net/rst.html) formatında yazılmış ve [Sphinx](http://www.sphinx-doc.org/) kullanılarak derlenmiştir. Belgeleri derlemek için öncelikle Sphinx'i kurmalısınız. Sphinx, Python'un 3.10 ve daha yukarı versiyonlarını desteklemektedir. +Bu belgeler [reStructuredText](http://docutils.sourceforge.net/rst.html) formatında yazılmış ve [Sphinx](http://www.sphinx-doc.org/) kullanılarak oluşturulmuştur. Belgeleri derlemek için öncelikle Sphinx'i kurmanız gerekmektedir. Sphinx, Python 3.10 ve üzeri sürümleri destekler. -Belgelere katkıda bulunmayı planlıyorsanız önce [`CONTRIBUTING.md`](CONTRIBUTING.md) dosyasına başvurun. +Belgelere katkıda bulunmayı düşünüyorsanız, öncelikle [`CONTRIBUTING.md`](CONTRIBUTING.md) dosyasını incelemenizi öneririz. -## Debian/Ubuntu +--- + +## Gereksinimler ve Kurulum + +### Debian/Ubuntu + +1. **Gerekli Araçlar:** + - Python 3.10 veya daha üstü sürüm + - `pip` + - `make` + +2. **Kurulum Adımları:** + + Projenin kök dizinine gidin ve gerekli kütüphaneleri şu komutla yükleyin: + + ```shell + python3 -m pip install -r requirements.txt + ``` + + Daha sonra belgeleri derlemek için şu komutu çalıştırın: -Bir Python3.10+ sürümünün, `pip`'in ve `make`'in sisteminizde kurulu olduğundan emin olduktan sonra projenin kök dizinine gidip bu komut ile gerekli kütüphaneleri kurabilirsiniz: + ```shell + make html + ``` -```shell -python3 -m pip install -r requirements.txt -``` + Belgeleri inşa ettikten sonra, `/scripts/move_documents.py` betiği, `/build/` dizinindeki gerekli dosyaları `/docs/` dizinine taşıyacaktır: -Daha sonra yine projenin kök dizinde bu komutu çalıştırarak belgeleri inşa edebilirsiniz: + ```shell + python3 ./scripts/move_documents.py + ``` -```shell -make html -``` + Sonuç olarak, oluşturulan belgeleri görüntülemek için `/docs/index.html` dosyasını tarayıcınızda açabilirsiniz. -Belgeleri inşa ettikten sonra `/scripts/move_documents.py` betiği `/build/` içindeki gerekli dosya ve klasörleri `/docs/` içine taşıyacaktır: +--- + +### Windows -```shell -python3 ./scripts/move_documents.py -``` +1. **Gerekli Araçlar:** + - Python 3.10 veya daha üstü sürüm + - PATH ortam değişkenine eklenmiş `python.exe` -Bu şekilde inşa ettiğiniz dökümanı görüntülemek için `/docs/index.html` dosyasını tarayıcınız ile açabilirsiniz. +2. **Kurulum Adımları:** -## Windows + Projenin kök dizinine gidin ve gerekli kütüphaneleri şu komutla yükleyin: -Python'un 3.10 veya daha yüksek bir sürümünün bilgisayarınızda kurulu olduğundan ve `python.exe`'nin PATH'de bulunduğundan emin olduktan sonra projenin kök dizinine gidip bu kodu ``cmd.exe``'de çalıştırarak gerekli kütüphaneleri kurabilirsiniz: + ```shell + python -m pip install -r requirements.txt + ``` -```shell -python -m pip install -r requirements.txt -``` + Yüklemenin başarılı olduğunu doğrulamak için aşağıdaki komutu çalıştırarak Sphinx sürümünü kontrol edin: -Yükleme işlemi başarıyla gerçekleşmiş ise şu komut size Sphinx'in versiyonunu verecektir: + ```shell + sphinx-build --version + ``` -```shell -sphinx-build --version -``` + Belgeleri inşa etmek için şu komutu çalıştırın: -Daha sonra yine projenin kök dizinde bu komutu çalıştırarak belgeleri inşa edebilirsiniz: + ```shell + make.bat html + ``` -```shell -make.bat html -``` + Belgeler oluşturulduktan sonra, `/scripts/move_documents.py` betiği ile dosyaları taşıyın: -Belgeleri inşa ettikten sonra `/scripts/move_documents.py` betiği `/build/` içindeki gerekli dosya ve klasörleri `/docs/` içine taşıyacaktır: + ```shell + python scripts/move_documents.py + ``` -```shell -python scripts/move_documents.py -``` + Oluşturulan belgeleri görmek için `/docs/index.html` dosyasını tarayıcınızda açabilirsiniz. -Bu şekilde inşa ettiğiniz dökümanı görüntülemek için `/docs/index.html` dosyasını tarayıcınız ile açabilirsiniz. +--- -## Diğer işletim sistemleri +### Diğer İşletim Sistemleri -Diğer işletim sistemlerinde Sphinx kurulumu ve ayrıntılı bilgi için [buraya](https://www.sphinx-doc.org/en/master/usage/installation.html) bakabilirsiniz. +Farklı işletim sistemlerinde Sphinx'in kurulumu hakkında detaylı bilgi için [Sphinx'in resmi kurulum belgelerini](https://www.sphinx-doc.org/en/master/usage/installation.html) inceleyebilirsiniz. --- -## Belgeleri diğer formatlarda inşa etme +## Diğer Formatlarda Belgeleri İnşa Etme + +Sphinx ile belgeleri farklı formatlarda oluşturabilirsiniz. Aşağıdaki adımları izleyerek belgeleri ihtiyaçlarınıza uygun şekilde inşa edebilirsiniz. -Önce yukarıdaki adımları takip edip Sphinx'in kurulumunu gerçekleştirin. +### Tek Parça HTML -Belgeleri diğer formatlarda inşa ettikten sonra da `/scripts/move_documents.py` betiğini çalıştırmayı unutmayın. +- **Debian/Ubuntu:** -### Tek parça HTML olarak inşa etme + ```shell + make singlehtml + ``` -Debian/Ubuntu'da: +- **Windows:** -```shell -make singlehtml -``` + ```shell + make.bat singlehtml + ``` -Windows'ta: + Oluşturulan HTML dosyası `/build/singlehtml/index.html` yolunda bulunacaktır. -```shell -make.bat singlehtml -``` +--- -HTML dosyası `/build/singlehtml/` dizininde `index.html` adı ile oluşacaktır. +### EPUB -### EPUB olarak inşa etme +- **Debian/Ubuntu:** -Debian/Ubuntu'da: + ```shell + make epub + ``` -```shell -make epub -``` +- **Windows:** -Windows'ta: + ```shell + make.bat epub + ``` -```shell -make.bat epub -``` + EPUB dosyası `/build/epub/Yazbel Python Belgeleri.epub` olarak kaydedilecektir. -EPUB dosyası `/build/epub/` dizininde `Yazbel Python Belgeleri.epub` adı ile oluşacaktır. +--- -### PDF olarak inşa etme +### PDF -Belgeleri PDF olarak inşa edebilmek için ``pdflatex`` uygulamasına ihtiyacınız olacak. [MikTeX](https://miktex.org/) veya [TeX Live](https://www.tug.org/texlive/) gibi bir TeX dağıtımını indirerek bu uygulamayı edinebilirsiniz. Bu dağıtımların belgerin inşası için gerekli eklentiler ile birlikte 800 Megabyte gibi bir disk alanı kaplayabileceğini unutmayın. TeX dağıtımının kurulumunda bir problem yaşarsanız [buraya](https://www.sphinx-doc.org/en/master/usage/builders/index.html#sphinx.builders.latex.LaTeXBuilder) başvurabilirsiniz. +PDF formatında belgeleri oluşturmak için bir TeX dağıtımı (ör. [MikTeX](https://miktex.org/) veya [TeX Live](https://www.tug.org/texlive/)) kurmanız gereklidir. Bu dağıtımlar genellikle ek eklentilerle birlikte kurulur ve yaklaşık 800 MB disk alanı gerektirir. -> Eğer Windows kullanıyorsanız ve [`winget`](https://github.com/microsoft/winget-cli) CLI uygulamasına sahipseniz MikTeX dağıtımını indirmek için bu yolu da izleyebilirsiniz: +> **Windows Kullanıcıları için:** Eğer [`winget`](https://github.com/microsoft/winget-cli) kuruluysa, MikTeX'i şu komutlarla yükleyebilirsiniz: > > ```shell > winget install MiKTeX.MiKTeX -> winget install StrawberryPerl.StrawberryPerl # MiKTeX aynı zamanda bir Perl kurulumu gerektirir +> winget install StrawberryPerl.StrawberryPerl # MikTeX ayrıca bir Perl kurulumu gerektirir > ``` -Uygun bir TeX dağıtımını kurduktan sonra `pdflatex`'in bulunduğu dizinin PATH'de bulunduğundan emin olun. +TeX dağıtımı kurulduktan sonra `pdflatex` uygulamasının PATH ortam değişkeninde yer aldığından emin olun. + +Belgeleri PDF formatında oluşturmak için: + +- **Debian/Ubuntu:** -Debian/Ubuntu'da: + ```shell + make latexpdf + ``` -```shell -make latexpdf -``` +- **Windows:** -Windows'ta: + ```shell + make.bat latexpdf + ``` -```shell -make.bat latexpdf -``` + Oluşturulan PDF dosyası `/build/latex/yazbelpythonbelgeleri.pdf` yolunda bulunacaktır. -Herhangi bir hata oluşmazsa PDF dosyası `/build/latex/` dizininde `yazbelpythonbelgeleri.pdf` adı ile oluşacaktır. diff --git a/scripts/color.py b/scripts/color.py index 58b818fa..e3d1cf23 100644 --- a/scripts/color.py +++ b/scripts/color.py @@ -1,12 +1,11 @@ -# see https://stackoverflow.com/questions/287871/how-do-i-print-colored-text-to-the-terminal -# and https://stackoverflow.com/questions/2048509/how-to-echo-with-different-colors-in-the-windows-command-line import os from functools import wraps -# System call to enable colors +# Enable colors on Windows systems if os.name == "nt": os.system("") +# ANSI escape codes for colors and styles class Colors: WHITE = '\033[97m' CYAN = '\033[96m' @@ -26,101 +25,145 @@ class Styles: END = '\033[0m' class Modify: - def __init__(self, color = '', style = ''): + """ + Context manager to apply a color and style temporarily. + """ + def __init__(self, color='', style=''): self.color = color self.style = style def __enter__(self): - print(self.color + self.style, end = "") + print(self.color + self.style, end="") return self def __exit__(self, exc_type, exc_val, exc_tb): - print(END, end = "") + print(END, end="") return False def guard(f): + """ + Decorator to ensure that ANSI reset (END) is applied + even if an exception occurs. + """ @wraps(f) - def g(*args, **kwargs): + def wrapper(*args, **kwargs): try: return f(*args, **kwargs) except BaseException as e: raise e finally: - print(END, end = "") - return g + print(END, end="") + return wrapper def with_modifiers(*modifiers): - def util(f): + """ + Decorator to apply colors/styles to the output of a function. + """ + def decorator(f): @wraps(f) @guard - def g(*args, **kwargs): - print(*modifiers, sep = "", end = "") - f(*args, **kwargs) - return g - return util + def wrapper(*args, **kwargs): + print(*modifiers, sep="", end="") + return f(*args, **kwargs) + return wrapper + return decorator @guard def guarded_print(*args, **kwargs): + """ + A safe print function that ensures ANSI reset. + """ print(*args, **kwargs) @with_modifiers(Colors.RED) def error(*args, **kwargs): + """ + Print an error message in red. + """ print(*args, **kwargs) @with_modifiers(Colors.GREEN) def success(*args, **kwargs): + """ + Print a success message in green. + """ print(*args, **kwargs) @with_modifiers(Colors.YELLOW) def warning(*args, **kwargs): + """ + Print a warning message in yellow. + """ print(*args, **kwargs) @with_modifiers(Styles.BOLD) def bold(*args, **kwargs): + """ + Print a message in bold. + """ print(*args, **kwargs) @with_modifiers(Colors.MAGENTA) def header(*args, **kwargs): + """ + Print a header in magenta. + """ print(*args, **kwargs) -def _split_without_removing(text, delimeter): - iterator = iter(text.split(delimeter)) +def _split_without_removing(text, delimiter): + """ + Split text by a delimiter without removing the delimiter. + """ + iterator = iter(text.split(delimiter)) + result = [] try: - l = [next(iterator)] + result.append(next(iterator)) while True: - i = next(iterator) - l.append(delimeter) - l.append(i) + result.append(delimiter) + result.append(next(iterator)) except StopIteration: - pass - return l - -def _split_multiple_without_removing(text, delimeters): - text = [text] - for delimeter in delimeters: + return result + +def _split_multiple_without_removing(text, delimiters): + """ + Split text by multiple delimiters without removing them. + """ + fragments = [text] + for delimiter in delimiters: temp = [] - for t in text: - temp.extend(_split_without_removing(t, delimeter)) - text = temp - return text - -def modify_text(text, words, color = '', replacement_rule = lambda x: x): - t = "" - for word in _split_multiple_without_removing(text, words): - if word in words: - t += color + replacement_rule(word) + END + for fragment in fragments: + temp.extend(_split_without_removing(fragment, delimiter)) + fragments = temp + return fragments + +def modify_text(text, words, color='', replacement_rule=lambda x: x): + """ + Modify parts of the text by applying colors and a replacement rule. + """ + result = "" + for fragment in _split_multiple_without_removing(text, words): + if fragment in words: + result += color + replacement_rule(fragment) + END else: - t += word - return t + result += fragment + return result -def modified_print(text, color_mapping, replacement_rule = lambda x: x): +def modified_print(text, color_mapping, replacement_rule=lambda x: x): + """ + Print text with multiple words highlighted in specified colors. + """ for words, color in color_mapping.items(): - text = modify_text(text, words, color, replacement_rule = replacement_rule) + text = modify_text(text, words, color, replacement_rule=replacement_rule) guarded_print(text) if __name__ == "__main__": - guarded_print(modify_text(modify_text("Hello world!", ["world"], Colors.GREEN + Styles.UNDERLINE), ["Hello"], Colors.BLUE + Styles.ITALIC)) + # Example usage + guarded_print(modify_text( + modify_text("Hello world!", ["world"], Colors.GREEN + Styles.UNDERLINE), + ["Hello"], Colors.BLUE + Styles.ITALIC + )) modified_print("Colors here!", { ("lors", "he"): Colors.YELLOW, ("Co", "re!"): Colors.RED, - }) \ No newline at end of file + }) + diff --git a/scripts/linkfix.py b/scripts/linkfix.py index 067809a8..f70e3d5d 100644 --- a/scripts/linkfix.py +++ b/scripts/linkfix.py @@ -3,93 +3,107 @@ from os.path import join, dirname from color import error, success, warning +# Dosya yollarını doğru ayarla file = os.path.realpath(__file__) base = dirname(dirname(file)) source = join(base, "source") linkcheck_build = join(base, "build", "linkcheck") output_file = join(linkcheck_build, "output.json") +# Çıktı dosyasını oku ve işle try: - with open(output_file, 'r', encoding = 'utf-8') as f: - # sphinx produces multiple records instead of a list of records for broken links - records = [json.loads(i) for i in f.readlines()] + with open(output_file, 'r', encoding='utf-8') as f: + # Sphinx, her kırık link için birden fazla kayıt üretir. + records = [json.loads(i) for i in f.readlines()] except FileNotFoundError: - error("ERROR: Run 'make linkcheck' first.") - exit() + error("ERROR: 'make linkcheck' komutunu çalıştırmalısınız.") + exit() - -warning("If you do a mistake while replacing the urls you can just terminate the script before it ends (CTRL + C).\n") +warning("Eğer bir URL'yi değiştirirken hata yaparsanız, scripti sonlandırmak için CTRL + C'yi kullanabilirsiniz.\n") cached_replacements = {} replacements = [] broken = 0 + +# Linkleri işlemeye başla for record in records: - status = record['status'] - uri = record['uri'] - - if status == "working": - continue - elif status == "unchecked": - ##print("INFO: Url {!r} is not checked.".format(uri)) - continue - elif status == "redirected": - # check whether the redirect is permanent - if record['code'] != 301: - ##print("INFO: Passing {!r} since the redirection is not permanent.".format(uri)) - continue - replace_with = record['info'] - elif status == "broken": - try: - replace_with = cached_replacements[uri] - except KeyError: - broken += 1 - print("File:\t" + record['filename']) - print("url:\t" + uri) - replace_with = input("The above url is broken, what do you want to change it with? You can also pass this question if you want to do nothing.\n > ") - if replace_with and not replace_with.isspace(): - success("Replaced the link.\n\n") - else: - warning("Passing.\n\n") - continue - elif status == "ignored": - continue - else: - raise ValueError("Unknown status for URL {!r}: {!r}".format(uri, status)) - - # used for :target: special casing - if replace_with == '.': - continue - - replacements.append((record, replace_with)) + status = record['status'] + uri = record['uri'] + + if status == "working": + continue + elif status == "unchecked": + continue + elif status == "redirected": + # Yönlendirmenin kalıcı olup olmadığını kontrol et + if record['code'] != 301: + continue + replace_with = record['info'] + elif status == "broken": + try: + replace_with = cached_replacements[uri] + except KeyError: + broken += 1 + print(f"Dosya: {record['filename']}") + print(f"URL: {uri}") + replace_with = input("Yukarıdaki URL kırık, yerine ne koymak istersiniz? Hiçbir şey yapmak istemiyorsanız boş bırakabilirsiniz.\n > ") + + if replace_with and not replace_with.isspace(): + success("Link değiştirildi.\n\n") + else: + warning("Geçiş yapılıyor.\n\n") + continue + elif status == "ignored": + continue + else: + raise ValueError(f"Unknown status for URL {uri!r}: {status}") + # :target: direktifine özel durum + if replace_with == '.': + continue + + replacements.append((record, replace_with)) + +# Değişiklikleri kontrol et if broken == 0: - success("All links are uptodate, not any broken links.") + success("Tüm bağlantılar güncel, kırık bağlantı yok.") elif len(replacements) == 0: - success(f"Didn't change any links out of {broken} broken ones.") + success(f"{broken} kırık linkten hiçbiri değiştirilmedi.") else: - warning("Make sure you made no mistake and press Enter to proceed.") - input() - success(f"Changed {len(replacements)}/{broken} broken links.") + warning("Yaptığınız değişikliklerden emin olduktan sonra Enter tuşuna basarak devam edin.") + input() + success(f"{broken} kırık linkten {len(replacements)}'i değiştirildi.") +# Dosya içeriğini güncelleme for record, replacement in replacements: - rst_file = join(source, record['filename']) - uri = record['uri'] + rst_file = join(source, record['filename']) + uri = record['uri'] + + try: + with open(rst_file, 'r', encoding="utf-8") as f: + data = f.read() + except FileNotFoundError: + warning(f"{rst_file} dosyası bulunamadı. Atlanıyor.\n") + continue - with open(rst_file, 'r', encoding = "utf-8") as f: - data = f.read() + # URL'yi dosya içinde arama + if uri not in data: + alternative_uri = uri.split(":", 1)[0] + if alternative_uri not in data: + warning(f"UYARI: URL '{uri}' {rst_file} dosyasının içinde bulunamadı. Bu, ':target:' direktifi ile ilgili olabilir.\n") + continue + uri = alternative_uri - if uri not in data: - alternative_uri = uri.split(":", 1)[0] - if alternative_uri not in data: - warning("WARNING: Can't find the URL {!r} in file {}:{}. Might be about a :target: directive.".format(uri, rst_file, record['lineno'])) - continue - uri = alternative_uri + # URL'yi değiştir + data = data.replace(uri, replacement) - data = data.replace(uri, replacement) + # :target: direktifini özel olarak kontrol et + if uri.startswith("_images") or uri.startswith("/images"): + data = "\n".join([i for i in data.split("\n") if ":target:" not in i]) - # special case :target: directives. - if uri.startswith("_images") or uri.startswith("/images"): - data = "\n".join([i for i in data.split("\n") if ":target:" not in i]) + try: + with open(rst_file, 'w', encoding="utf-8") as f: + f.write(data) + except Exception as e: + error(f"{rst_file} dosyasına yazılırken hata oluştu: {e}") - with open(rst_file, 'w', encoding = "utf-8") as f: - f.write(data) diff --git a/scripts/manage.py b/scripts/manage.py index 24e687be..57a08ebf 100644 --- a/scripts/manage.py +++ b/scripts/manage.py @@ -4,6 +4,10 @@ from color import error, warning, success, Styles, Colors, modify_text from difflib import SequenceMatcher import sys +import subprocess +from threading import Thread, Event +from time import sleep +import webbrowser root = os.path.abspath(".") shared = join(root, "shared") @@ -11,237 +15,230 @@ index_file = join(root, "docs", "index.html") class App: - def __init__(self): - self.procedures = {} - self.configurations = { - "default": None, - } - - def configure(self, **kwargs): - invalid = set(kwargs) - set(self.configurations) - if invalid: - raise ValueError(f"The followings are not valid configuration keys: {', '.join(invalid)}.") - self.configurations.update(kwargs) - - def command(self, *cli_args): - def util(f): - @wraps(f) - def g(*args, **kwargs): - f(self, *args, **kwargs) - self.procedures[g.__name__] = (g, cli_args) - return g - return util - - def run(self, args): - if len(args) == 0: - if self.configurations["default"] is None: - error(f"No argument is passed.") - return - command = self.configurations["default"] - arg = () - elif len(args) > 2: - error(f"No more than two args is required.") - return - else: - command, *arg = args - - try: - procedure, expected_args = self.get_command(command) - except ValueError: - return - - if not arg: - try: - procedure() - except TypeError as e: - raise e - error(f"{command!r} command requires an argument but none were given.") - return - - arg = arg[0] - if arg in expected_args: - procedure(arg) - else: - if not expected_args: - error(f"{command!r} command takes no argument.") - return - match = self.find_similar(arg, expected_args) - error(f"{command!r} command have no argument named {arg!r}.") - if match is not None: - error(f"Did you perhaps meant {match!r}?") - - @classmethod - def find_similar(cls, name, pool): - matcher = SequenceMatcher() - matcher.set_seq1(name.casefold()) - ratios = {} - for p in pool: - matcher.set_seq2(p.casefold()) - ratio = matcher.ratio() - ratios[ratio] = p - m = max(ratios) - if m > 0.6: - return ratios[m] - else: - return None - - def get_command(self, name): - try: - return self.procedures[name] - except KeyError: - match = self.find_similar(name, self.procedures) - error(f"No command named {name!r}.") - if match is not None: - error(f"Did you perhaps meant {match!r}?") - raise ValueError() - - @classmethod - def create_status_animation(cls, style, msg): - from threading import Thread, Event - from time import sleep - - def status_animation(): - style(msg, end="", flush = True) - dot = 1 - while True: - if dot > 3: - style("\r" + msg + " " * dot + "\b" * dot, end="", flush = True) - dot = 1 - style(".", end = "", flush = True) - dot += 1 - - if event.is_set(): - style("\r" + " " * (len(msg) + dot-1) + "\r", end="") - break - sleep(0.5) - - event = Event() - t = Thread(daemon = True, target=status_animation) - t.start() - - def finish(): - event.set() - t.join() - - return finish - - @classmethod - def call(cls, cmd, jobname): - import subprocess - - stop_animation = cls.create_status_animation(print, f"Building {jobname}") - - err = subprocess.PIPE - out = subprocess.PIPE - p = subprocess.run(cmd, shell=True, stdout=out, stderr=err) - - stop_animation() - - error(p.stderr.decode(), end = "") - if not p.stderr: - success(f"Built {jobname}.") - return True - else: - warning(f"There were errors while building {jobname}.") - return False + def __init__(self): + self.procedures = {} + self.configurations = { + "default": None, + } + + def configure(self, **kwargs): + invalid = set(kwargs) - set(self.configurations) + if invalid: + raise ValueError(f"The following are not valid configuration keys: {', '.join(invalid)}.") + self.configurations.update(kwargs) + + def command(self, *cli_args): + def util(f): + @wraps(f) + def g(*args, **kwargs): + f(self, *args, **kwargs) + self.procedures[g.__name__] = (g, cli_args) + return g + return util + + def run(self, args): + if len(args) == 0: + if self.configurations["default"] is None: + error(f"No argument is passed.") + return + command = self.configurations["default"] + arg = () + elif len(args) > 2: + error(f"No more than two args are allowed.") + return + else: + command, *arg = args + + try: + procedure, expected_args = self.get_command(command) + except ValueError: + return + + if not arg: + try: + procedure() + except TypeError: + error(f"{command!r} command requires an argument but none were given.") + return + + arg = arg[0] + if arg in expected_args: + procedure(arg) + else: + if not expected_args: + error(f"{command!r} command takes no arguments.") + return + match = self.find_similar(arg, expected_args) + error(f"{command!r} command does not have an argument named {arg!r}.") + if match is not None: + error(f"Did you perhaps mean {match!r}?") + + @classmethod + def find_similar(cls, name, pool): + matcher = SequenceMatcher() + matcher.set_seq1(name.casefold()) + ratios = {} + for p in pool: + matcher.set_seq2(p.casefold()) + ratio = matcher.ratio() + ratios[ratio] = p + m = max(ratios) + if m > 0.6: + return ratios[m] + else: + return None + + def get_command(self, name): + try: + return self.procedures[name] + except KeyError: + match = self.find_similar(name, self.procedures) + error(f"No command named {name!r}.") + if match is not None: + error(f"Did you perhaps mean {match!r}?") + raise ValueError() + + @classmethod + def create_status_animation(cls, style, msg): + def status_animation(): + style(msg, end="", flush=True) + dot = 1 + while True: + if dot > 3: + style("\r" + msg + " " * dot + "\b" * dot, end="", flush=True) + dot = 1 + style(".", end="", flush=True) + dot += 1 + if event.is_set(): + style("\r" + " " * (len(msg) + dot - 1) + "\r", end="") + break + sleep(0.5) + + event = Event() + t = Thread(daemon=True, target=status_animation) + t.start() + + def finish(): + event.set() + t.join() + + return finish + + @classmethod + def call(cls, cmd, jobname): + stop_animation = cls.create_status_animation(print, f"Building {jobname}") + + p = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + stop_animation() + + stderr = p.stderr.decode() + if stderr: + error(stderr, end="") + warning(f"There were errors while building {jobname}.") + return False + else: + success(f"Built {jobname}.") + return True app = App() -app.configure(default = "help") +app.configure(default="help") @app.command("release", "dev", "all") -def build(app, job = "debug"): - """Builds the docs. Builds them in all available formats if [release] or [all] argument is given. - * Increases the project version if [release] argument is given. - * Opens the docs/index.html in browser if [dev] argument is given. - * Moves them to where they are needed if [release] or [dev] argument is given.""" - - # should we do that at the start so that it affects this release or at the end so that it doesn't run if there is an exception? - if job == "release": - version('patch') - - print("Starting the build process.") - p = app.call("make html", "HTML") - - if job in ("release", "all"): - p = app.call("make singlehtml", "HTML (single file)") - p = app.call("make latexpdf", "PDF") # TODO: this job never finishes because latexpdf waits for input on syntax errors - p = app.call("make epub", "EPUB") - - if job in ("debug", "all"): - return - - import move_documents - if job == "dev": - view() +def build(app, job="debug"): + """Builds the docs. Builds them in all available formats if [release] or [all] argument is given. + * Increases the project version if [release] argument is given. + * Opens the docs/index.html in browser if [dev] argument is given. + * Moves them to where they are needed if [release] or [dev] argument is given.""" + + # Perform version upgrade if needed + if job == "release": + version('patch') + + print("Starting the build process.") + if not app.call("make html", "HTML"): + return + + if job in ("release", "all"): + if not app.call("make singlehtml", "HTML (single file)"): + return + if not app.call("make latexpdf", "PDF"): + return + if not app.call("make epub", "EPUB"): + return + + if job in ("debug", "all"): + return + + import move_documents + if job == "dev": + view() @app.command() def view(app): - "Opens the docs/index.html in browser." - import webbrowser - webbrowser.open(index_file, new=0, autoraise=True) - success("Opened the docs/index.html in browser.") - + "Opens the docs/index.html in browser." + webbrowser.open(index_file, new=0, autoraise=True) + success("Opened the docs/index.html in browser.") + @app.command() def checklinks(app): - "Check the links and fix them manually." - print("Checking the links.") - p = app.call("make linkcheck", "linkcheck") - if p: - import linkfix + "Check the links and fix them manually." + print("Checking the links.") + if app.call("make linkcheck", "linkcheck"): + import linkfix @app.command("major", "minor", "patch", "downgrade") -def version(app, field = "display"): - """Upgrades or downgrades the project version with respect to the specified argument ([major], [minor], [patch] or [downgrade]). - * Displays the current version if no argument is passed.""" - with open(version_file, "r") as f: - versions = list(map(lambda x: x[:-1] if x.endswith("\n") else x, f)) - - if field == "display": - return print("Project version:", versions[-1]) - - if field == "downgrade": - if len(versions) == 1: - error("Can't downgrade when there is a single recorded version.") - return - versions.pop() - success(f"Downgraded the version to {versions[-1]}.") - else: - version = list(map(int, versions[-1].split("."))) - index = ("major", "minor", "patch").index(field) - - version[index] += 1 - for i in range(index + 1, 3): - version[i] = 0 - version = ".".join(map(str, version)) - versions.append(version) - success(f"Upgraded the version to {version}.") - - with open(version_file, "w") as f: - f.write("\n".join(versions)) +def version(app, field="display"): + """Upgrades or downgrades the project version with respect to the specified argument ([major], [minor], [patch] or [downgrade]). + * Displays the current version if no argument is passed.""" + with open(version_file, "r") as f: + versions = list(map(lambda x: x.strip(), f)) + + if field == "display": + return print("Project version:", versions[-1]) + + if field == "downgrade": + if len(versions) == 1: + error("Can't downgrade when there is a single recorded version.") + return + versions.pop() + success(f"Downgraded the version to {versions[-1]}.") + else: + version = list(map(int, versions[-1].split("."))) + index = ("major", "minor", "patch").index(field) + + version[index] += 1 + for i in range(index + 1, 3): + version[i] = 0 + version = ".".join(map(str, version)) + versions.append(version) + success(f"Upgraded the version to {version}.") + + with open(version_file, "w") as f: + f.write("\n".join(versions)) def highlight_arguments(procedure): - doc = procedure[0].__doc__ - words = tuple(map(lambda x: f"[{x}]", procedure[1])) - return modify_text(doc, words = words, color = Styles.UNDERLINE + Colors.CYAN, replacement_rule = lambda x: x[1:-1]) + doc = procedure[0].__doc__ + words = tuple(map(lambda x: f"[{x}]", procedure[1])) + return modify_text(doc, words=words, color=Styles.UNDERLINE + Colors.CYAN, replacement_rule=lambda x: x[1:-1]) @app.command(*app.procedures, "help") -def help(app, method = None): - """Displays a help message for the given argument.""" - if method is None: - print("Valid arguments for the application:\n") - for i in app.procedures: - print(f"- {i:<20}" + highlight_arguments(app.procedures[i])) - print() - else: - print(f"{method}:", highlight_arguments(app.procedures[method])) +def help(app, method=None): + """Displays a help message for the given argument.""" + if method is None: + print("Valid arguments for the application:\n") + for i in app.procedures: + print(f"- {i:<20}" + highlight_arguments(app.procedures[i])) + print() + else: + print(f"{method}:", highlight_arguments(app.procedures[method])) if __name__ == "__main__": - import sys - args = sys.argv[1:] - try: - app.run(args) - except KeyboardInterrupt: - print() - warning("Received CTRL+C, shutting down.") + args = sys.argv[1:] + try: + app.run(args) + except KeyboardInterrupt: + print() + warning("Received CTRL+C, shutting down.") else: - raise ImportError("This file is not supposed to be imported.") \ No newline at end of file + raise ImportError("This file is not supposed to be imported.") diff --git a/scripts/move_documents.py b/scripts/move_documents.py index 2bdd73bb..641f8e9e 100644 --- a/scripts/move_documents.py +++ b/scripts/move_documents.py @@ -3,7 +3,27 @@ import shutil from color import error, success, warning -# TODO: put these in a utils module or something +# Utility functions +def check_dir(d, hint=""): + """Check if directory exists, if not print error and exit.""" + if not os.path.exists(d) or not os.path.isdir(d): + error(f"ERROR: '{d}' directory is not found. Can't copy files.\n" + hint) + exit() + +def backup_file(file, backup_dir): + """Backup file before any potentially destructive operation.""" + if not exists(backup_dir): + os.makedirs(backup_dir) # Ensure backup directory exists + backup_path = join(backup_dir, os.path.basename(file)) + try: + shutil.copy2(file, backup_path) + print(f"Backup created for {file} at {backup_path}") + except FileNotFoundError: + warning(f"Warning: The file {file} to be backed up was not found.") + except Exception as e: + warning(f"Warning: Failed to back up {file}. Error: {e}") + +# Paths and project setup file = realpath(__file__) script_dir = dirname(file) root = dirname(script_dir) @@ -11,46 +31,54 @@ target = join(build, 'html') docs = join(root, 'docs') -def check_dir(d, hint = ""): - if not os.path.exists(d) or not os.path.isdir(d): - error(f"ERROR: '{d}' directory is not found. Can't copy files.\n" + hint) - exit() - +# Check directories check_dir(build, "Hint: Are you sure you built the docs as described in BUILDING.md?") check_dir(target, "Hint: Are you sure you built the HTML files as described in BUILDING.md?") -check_dir(docs, "Hint: Did you clone the repository succesfully?") +check_dir(docs, "Hint: Did you clone the repository successfully?") +# Project name and output file paths project = 'Yazbel Python Belgeleri' - epub_file = join(build, 'epub', '{}.epub'.format(project)) pdf_file = join(build, 'latex', '{}.pdf'.format(project.replace(' ', '').lower())) html_file = join(build, 'singlehtml', 'index.html') output_name = 'YazbelPythonProgramlamaDiliBelgeleri' + +# Process each file for file in [epub_file, pdf_file, html_file]: - ext = file.rsplit('.', 1)[1] - target_name = output_name + "." + ext - file_type = "HTML (single file)" if ext == "html" else ext.upper() - try: - shutil.copy2(file, join(target, target_name)) - except FileNotFoundError: - warning(f"Passing {file_type} file since it is not found in the build directory. Preserving the old {file_type} file instead.") - # copy the old build so that they don't get deleted - try: - shutil.copy2(join(docs, target_name), join(target, target_name)) - except FileNotFoundError: - error(f"ERROR: Couldn't find the old {file_type} file.\nHint: Did you perhaps delete the files that were in the docs directory?") - exit() - else: - print(f"Copied {file_type} file to the docs directory.") - - -# fix this, we shouldn't be deleting it altogether -# also we might want to preserve the output files (PDF, EPUB) in a permanent directory in case the script gets killed just after deleting docs -if exists(docs): - shutil.rmtree(docs) + ext = file.rsplit('.', 1)[1] + target_name = output_name + "." + ext + file_type = "HTML (single file)" if ext == "html" else ext.upper() + + try: + # Copy new file + shutil.copy2(file, join(target, target_name)) + print(f"Copied {file_type} file to the docs directory.") + except FileNotFoundError: + # File missing in the build, try to use the old version from docs + warning(f"Passing {file_type} file since it is not found in the build directory. Preserving the old {file_type} file instead.") + try: + backup_file(join(docs, target_name), join(root, 'backup_files')) # Backup the old file before copying it + shutil.copy2(join(docs, target_name), join(target, target_name)) + print(f"Restored {file_type} from docs directory.") + except FileNotFoundError: + error(f"ERROR: Couldn't find the old {file_type} file.\nHint: Did you perhaps delete the files that were in the docs directory?") + exit() + except Exception as e: + error(f"ERROR: Failed to copy {file_type} file. Error: {e}") + exit() +# Handle the removal and copying of directories +if exists(docs): + try: + shutil.rmtree(docs) + print(f"Removed existing docs directory.") + except Exception as e: + warning(f"Warning: Failed to remove docs directory. Error: {e}") -shutil.copytree(target, docs, copy_function = shutil.copy2) +# Copy the new HTML files to docs +shutil.copytree(target, docs, copy_function=shutil.copy2) print("Copied the hosted HTML files to the docs directory.") -success("Successfully copied the required files.") \ No newline at end of file + +# Final success message +success("Successfully copied the required files.")