Task 348: .KV File Format

Task 348: .KV File Format

File Format Specifications for the .KV File Format

The .KV file format is a text-based declarative language used by the Kivy framework, an open-source Python library for developing multitouch applications. It is used to define widget trees, property bindings, and UI layouts in a concise syntax. The format is not binary; it is plain text, typically encoded in UTF-8, and follows a structure similar to CSS or YAML with Python-like expressions. Specifications are detailed in the official Kivy documentation.

  1. List of All Properties Intrinsic to This File Format

Based on the specifications, the following are the key intrinsic properties and structural elements of the .kv file format. These define its syntax, structure, and behavior:

  • File Extension: Must be .kv.
  • Encoding: Typically UTF-8, though issues with encoding (e.g., cp1252 on Windows) have been noted in practice.
  • Header: Starts with a mandatory Kivy version declaration, e.g., #:kivy 1.0 or higher, specifying compatibility.
  • Directives: Special comment-based instructions:
  • #:import alias package: Imports Python modules or classes.
  • #:set key value: Sets global constants.
  • #:include [force] file: Includes external .kv files.
  • Indentation: Consistent spacing (spaces preferred, no mixing with tabs), multiple of the first indent level; used for delimiting rules and children.
  • Root Widget: A single top-level widget definition without brackets, e.g., MyRootWidget:, representing the base of the UI tree.
  • Class Rules: Definitions for widget classes, e.g., <MyWidget>: or multiple <Rule1,Rule2>:, allowing style sharing.
  • Dynamic Classes: On-the-fly widget creation with inheritance, e.g., <NewWidget@BaseClass>: or multiple inheritance <NewWidget@ButtonBehavior+Label>:.
  • Templates: Deprecated feature for reusable widget trees, e.g., [TemplateName@BaseClass]:.
  • Property Definitions: Assignments like property_name: value, where values are Python expressions; automatically creates properties if undefined.
  • Event Bindings: Handlers like on_property: callback(), supporting multi-line statements and args for event data.
  • Canvas Instructions: Graphical definitions under canvas:, canvas.before:, or canvas.after:, e.g., Color: rgba: 1, .3, .8, .5.
  • IDs: Widget references like id: my_id, stored as weak references in ids dictionary; use __self__ for strong references.
  • Expressions and Bindings: Python expressions in values, including f-strings (since v2.1.0), with automatic observation for changes (Observer pattern).
  • Redefining Styles: Use - prefix to clear previous rules, e.g., <-MyWidget>:.
  • Order of Application: KV rules applied after Python kwargs, with constants not overwriting explicit values.

These properties ensure the format's declarative nature, allowing UI separation from code.

  1. Two Direct Download Links for .KV Files
  1. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .KV File Dump

Here's a self-contained HTML snippet with embedded JavaScript that can be embedded in a blog post (e.g., on Ghost platform). It creates a drag-and-drop area for a .kv file, parses it to extract the intrinsic properties listed above, and dumps them to the screen in a readable format. The parser is basic: it scans lines for patterns matching the properties (e.g., headers, directives, rules).

Drag and drop a .kv file here
  1. Python Class for .KV File Handling

Here's a Python class that can open, parse (decode), read, print the properties, and write a modified .kv file. It uses a simple line-based parser similar to the JS one.

import re

class KVFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.properties = {}
        self.content = ''
        self.parse()

    def parse(self):
        with open(self.filepath, 'r', encoding='utf-8') as f:
            self.content = f.read()
            lines = self.content.split('\n')
            self.properties = {
                'header': '',
                'directives': {'imports': [], 'sets': [], 'includes': []},
                'root_widget': '',
                'class_rules': [],
                'dynamic_classes': [],
                'templates': [],
                'property_defs': [],
                'event_bindings': [],
                'canvas_instructions': [],
                'ids': [],
                'expressions': []
            }
            in_canvas = False
            for line in lines:
                line = line.strip()
                if line.startswith('#:kivy'):
                    self.properties['header'] = line
                elif line.startswith('#:import'):
                    self.properties['directives']['imports'].append(line)
                elif line.startswith('#:set'):
                    self.properties['directives']['sets'].append(line)
                elif line.startswith('#:include'):
                    self.properties['directives']['includes'].append(line)
                elif re.match(r'^[A-Z][a-zA-Z0-9_]*:$', line) and not in_canvas:
                    self.properties['root_widget'] = line[:-1]
                elif re.match(r'^<[a-zA-Z0-9_, ]+>:$', line):
                    self.properties['class_rules'].append(line)
                elif re.match(r'^<[a-zA-Z0-9_]+@[a-zA-Z0-9_+]+>:$', line):
                    self.properties['dynamic_classes'].append(line)
                elif re.match(r'^\[[a-zA-Z0-9_]+@[a-zA-Z0-9_,+]+]:$', line):
                    self.properties['templates'].append(line)
                elif ':' in line and not line.startswith('on_') and not in_canvas:
                    self.properties['property_defs'].append(line)
                elif line.startswith('on_') and ':' in line:
                    self.properties['event_bindings'].append(line)
                elif line in ['canvas:', 'canvas.before:', 'canvas.after:']:
                    in_canvas = True
                    self.properties['canvas_instructions'].append(line)
                elif in_canvas and (line.startswith('-') or line == 'Clear'):
                    in_canvas = False
                elif in_canvas:
                    self.properties['canvas_instructions'].append(line)
                elif line.startswith('id:'):
                    self.properties['ids'].append(line)
                if re.search(r'(self\.|root\.|app\.|f["\'])', line):
                    self.properties['expressions'].append(line)

    def print_properties(self):
        import json
        print(json.dumps(self.properties, indent=4))

    def write(self, new_filepath=None):
        if new_filepath is None:
            new_filepath = self.filepath
        with open(new_filepath, 'w', encoding='utf-8') as f:
            f.write(self.content)  # For now, writes original; can modify self.content for changes

# Example usage:
# handler = KVFileHandler('example.kv')
# handler.print_properties()
# handler.write('modified.kv')
  1. Java Class for .KV File Handling

Here's a Java class that does the same: open, parse, print properties to console, and write.

import java.io.*;
import java.util.*;
import java.util.regex.Pattern;

public class KVFileHandler {
    private String filepath;
    private Map<String, Object> properties;
    private String content;

    public KVFileHandler(String filepath) {
        this.filepath = filepath;
        properties = new HashMap<>();
        parse();
    }

    private void parse() {
        try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line).append("\n");
            }
            content = sb.toString();
            String[] lines = content.split("\n");
            Map<String, List<String>> directives = new HashMap<>();
            directives.put("imports", new ArrayList<>());
            directives.put("sets", new ArrayList<>());
            directives.put("includes", new ArrayList<>());
            properties.put("directives", directives);
            properties.put("class_rules", new ArrayList<String>());
            properties.put("dynamic_classes", new ArrayList<String>());
            properties.put("templates", new ArrayList<String>());
            properties.put("property_defs", new ArrayList<String>());
            properties.put("event_bindings", new ArrayList<String>());
            properties.put("canvas_instructions", new ArrayList<String>());
            properties.put("ids", new ArrayList<String>());
            properties.put("expressions", new ArrayList<String>());
            properties.put("header", "");
            properties.put("root_widget", "");

            boolean inCanvas = false;
            for (String l : lines) {
                line = l.trim();
                if (line.startsWith("#:kivy")) {
                    properties.put("header", line);
                } else if (line.startsWith("#:import")) {
                    ((List<String>) directives.get("imports")).add(line);
                } else if (line.startsWith("#:set")) {
                    ((List<String>) directives.get("sets")).add(line);
                } else if (line.startsWith("#:include")) {
                    ((List<String>) directives.get("includes")).add(line);
                } else if (Pattern.matches("^[A-Z][a-zA-Z0-9_]*:$", line) && !inCanvas) {
                    properties.put("root_widget", line.replace(":", ""));
                } else if (Pattern.matches("^<[a-zA-Z0-9_, ]+>:$", line)) {
                    ((List<String>) properties.get("class_rules")).add(line);
                } else if (Pattern.matches("^<[a-zA-Z0-9_]+@[a-zA-Z0-9_+]+>:$", line)) {
                    ((List<String>) properties.get("dynamic_classes")).add(line);
                } else if (Pattern.matches("^\\[[a-zA-Z0-9_]+@[a-zA-Z0-9_,+]+]:$", line)) {
                    ((List<String>) properties.get("templates")).add(line);
                } else if (line.contains(":") && !line.startsWith("on_") && !inCanvas) {
                    ((List<String>) properties.get("property_defs")).add(line);
                } else if (line.startsWith("on_") && line.contains(":")) {
                    ((List<String>) properties.get("event_bindings")).add(line);
                } else if (line.equals("canvas:") || line.equals("canvas.before:") || line.equals("canvas.after:")) {
                    inCanvas = true;
                    ((List<String>) properties.get("canvas_instructions")).add(line);
                } else if (inCanvas && (line.startsWith("-") || line.equals("Clear"))) {
                    inCanvas = false;
                } else if (inCanvas) {
                    ((List<String>) properties.get("canvas_instructions")).add(line);
                } else if (line.startsWith("id:")) {
                    ((List<String>) properties.get("ids")).add(line);
                }
                if (Pattern.matches(".*(self\\.|root\\.|app\\.|f[\"']).*", line)) {
                    ((List<String>) properties.get("expressions")).add(line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        System.out.println(properties);  // Simple print; use Gson for pretty JSON if needed
    }

    public void write(String newFilepath) {
        if (newFilepath == null) newFilepath = filepath;
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(newFilepath))) {
            writer.write(content);  // Writes original; modify content for changes
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     KVFileHandler handler = new KVFileHandler("example.kv");
    //     handler.printProperties();
    //     handler.write("modified.kv");
    // }
}
  1. JavaScript Class for .KV File Handling

Here's a JavaScript class (Node.js compatible) for opening, parsing, printing to console, and writing.

const fs = require('fs');

class KVFileHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.properties = {};
    this.content = '';
    this.parse();
  }

  parse() {
    this.content = fs.readFileSync(this.filepath, 'utf-8');
    const lines = this.content.split('\n');
    this.properties = {
      header: '',
      directives: { imports: [], sets: [], includes: [] },
      root_widget: '',
      class_rules: [],
      dynamic_classes: [],
      templates: [],
      property_defs: [],
      event_bindings: [],
      canvas_instructions: [],
      ids: [],
      expressions: []
    };
    let inCanvas = false;
    lines.forEach((line) => {
      line = line.trim();
      if (line.startsWith('#:kivy')) this.properties.header = line;
      else if (line.startsWith('#:import')) this.properties.directives.imports.push(line);
      else if (line.startsWith('#:set')) this.properties.directives.sets.push(line);
      else if (line.startsWith('#:include')) this.properties.directives.includes.push(line);
      else if (/^[A-Z][a-zA-Z0-9_]*:$/.test(line) && !inCanvas) this.properties.root_widget = line.replace(':', '');
      else if (/^<[a-zA-Z0-9_, ]+>:$/.test(line)) this.properties.class_rules.push(line);
      else if (/^<[a-zA-Z0-9_]+@[a-zA-Z0-9_+]+>:$/.test(line)) this.properties.dynamic_classes.push(line);
      else if (/^\[[a-zA-Z0-9_]+@[a-zA-Z0-9_,+]+]:$/.test(line)) this.properties.templates.push(line);
      else if (line.includes(':') && !line.startsWith('on_') && !inCanvas) this.properties.property_defs.push(line);
      else if (line.startsWith('on_') && line.includes(':')) this.properties.event_bindings.push(line);
      else if (['canvas:', 'canvas.before:', 'canvas.after:'].includes(line)) {
        inCanvas = true;
        this.properties.canvas_instructions.push(line);
      } else if (inCanvas && (line.startsWith('-') || line === 'Clear')) inCanvas = false;
      else if (inCanvas) this.properties.canvas_instructions.push(line);
      else if (line.startsWith('id:')) this.properties.ids.push(line);
      if (/(self\.|root\.|app\.|f["'])/.test(line)) this.properties.expressions.push(line);
    });
  }

  printProperties() {
    console.log(JSON.stringify(this.properties, null, 2));
  }

  write(newFilepath = this.filepath) {
    fs.writeFileSync(newFilepath, this.content, 'utf-8');
  }
}

// Example usage:
// const handler = new KVFileHandler('example.kv');
// handler.printProperties();
// handler.write('modified.kv');
  1. C++ Class for .KV File Handling

Here's a C++ class (using std::regex for parsing) that opens, parses, prints to console, and writes.

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <regex>

class KVFileHandler {
private:
    std::string filepath;
    std::map<std::string, std::string> simpleProps;
    std::map<std::string, std::vector<std::string>> listProps;
    std::map<std::string, std::map<std::string, std::vector<std::string>>> nestedProps;
    std::string content;

public:
    KVFileHandler(const std::string& fp) : filepath(fp) {
        parse();
    }

    void parse() {
        std::ifstream file(filepath);
        if (!file) {
            std::cerr << "Error opening file" << std::endl;
            return;
        }
        std::string line;
        content = "";
        bool inCanvas = false;
        std::map<std::string, std::vector<std::string>> directives;
        directives["imports"] = {};
        directives["sets"] = {};
        directives["includes"] = {};
        nestedProps["directives"] = directives;
        listProps["class_rules"] = {};
        listProps["dynamic_classes"] = {};
        listProps["templates"] = {};
        listProps["property_defs"] = {};
        listProps["event_bindings"] = {};
        listProps["canvas_instructions"] = {};
        listProps["ids"] = {};
        listProps["expressions"] = {};
        simpleProps["header"] = "";
        simpleProps["root_widget"] = "";

        while (std::getline(file, line)) {
            content += line + "\n";
            line = std::regex_replace(line, std::regex("^ +| +$|( )+"), "$1");  // Trim
            if (line.find("#:kivy") == 0) {
                simpleProps["header"] = line;
            } else if (line.find("#:import") == 0) {
                nestedProps["directives"]["imports"].push_back(line);
            } else if (line.find("#:set") == 0) {
                nestedProps["directives"]["sets"].push_back(line);
            } else if (line.find("#:include") == 0) {
                nestedProps["directives"]["includes"].push_back(line);
            } else if (std::regex_match(line, std::regex("^[A-Z][a-zA-Z0-9_]*:$")) && !inCanvas) {
                simpleProps["root_widget"] = line.substr(0, line.size() - 1);
            } else if (std::regex_match(line, std::regex("^<[a-zA-Z0-9_, ]+>:$"))) {
                listProps["class_rules"].push_back(line);
            } else if (std::regex_match(line, std::regex("^<[a-zA-Z0-9_]+@[a-zA-Z0-9_+]+>:$"))) {
                listProps["dynamic_classes"].push_back(line);
            } else if (std::regex_match(line, std::regex("^\\[[a-zA-Z0-9_]+@[a-zA-Z0-9_,+]+]:$"))) {
                listProps["templates"].push_back(line);
            } else if (line.find(':') != std::string::npos && line.find("on_") != 0 && !inCanvas) {
                listProps["property_defs"].push_back(line);
            } else if (line.find("on_") == 0 && line.find(':') != std::string::npos) {
                listProps["event_bindings"].push_back(line);
            } else if (line == "canvas:" || line == "canvas.before:" || line == "canvas.after:") {
                inCanvas = true;
                listProps["canvas_instructions"].push_back(line);
            } else if (inCanvas && (line[0] == '-' || line == "Clear")) {
                inCanvas = false;
            } else if (inCanvas) {
                listProps["canvas_instructions"].push_back(line);
            } else if (line.find("id:") == 0) {
                listProps["ids"].push_back(line);
            }
            if (std::regex_search(line, std::regex("(self\\.|root\\.|app\\.|f[\"'])"))) {
                listProps["expressions"].push_back(line);
            }
        }
        file.close();
    }

    void printProperties() {
        // Simple console print (not JSON, as C++ std doesn't have built-in)
        std::cout << "header: " << simpleProps["header"] << std::endl;
        std::cout << "root_widget: " << simpleProps["root_widget"] << std::endl;
        std::cout << "directives.imports: ";
        for (auto& v : nestedProps["directives"]["imports"]) std::cout << v << " ";
        std::cout << std::endl;
        // Repeat for others...
        std::cout << "directives.sets: ";
        for (auto& v : nestedProps["directives"]["sets"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "directives.includes: ";
        for (auto& v : nestedProps["directives"]["includes"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "class_rules: ";
        for (auto& v : listProps["class_rules"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "dynamic_classes: ";
        for (auto& v : listProps["dynamic_classes"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "templates: ";
        for (auto& v : listProps["templates"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "property_defs: ";
        for (auto& v : listProps["property_defs"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "event_bindings: ";
        for (auto& v : listProps["event_bindings"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "canvas_instructions: ";
        for (auto& v : listProps["canvas_instructions"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "ids: ";
        for (auto& v : listProps["ids"]) std::cout << v << " ";
        std::cout << std::endl;
        std::cout << "expressions: ";
        for (auto& v : listProps["expressions"]) std::cout << v << " ";
        std::cout << std::endl;
    }

    void write(const std::string& newFilepath = "") {
        std::string outPath = newFilepath.empty() ? filepath : newFilepath;
        std::ofstream file(outPath);
        if (file) {
            file << content;
            file.close();
        } else {
            std::cerr << "Error writing file" << std::endl;
        }
    }
};

// Example usage:
// int main() {
//     KVFileHandler handler("example.kv");
//     handler.printProperties();
//     handler.write("modified.kv");
//     return 0;
// }